mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
improve endpoint self-documentation, add explicit operation IDs
This commit is contained in:
@@ -55,17 +55,31 @@ v2_app = FastAPI(
|
||||
"name": "blocks",
|
||||
"description": "Discover available building blocks",
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"description": "Cross-domain hybrid search across agents, blocks, and docs",
|
||||
},
|
||||
{
|
||||
"name": "marketplace",
|
||||
"description": "Browse agents and creators, manage submissions",
|
||||
},
|
||||
{
|
||||
"name": "library",
|
||||
"description": "Access your agent library and execute agents",
|
||||
"description": (
|
||||
"Manage your agent library (agents and presets), "
|
||||
"execute agents, organize with folders"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "presets",
|
||||
"description": "Agent execution presets with webhook triggers",
|
||||
},
|
||||
{
|
||||
"name": "runs",
|
||||
"description": "Monitor execution runs and human-in-the-loop reviews",
|
||||
"description": (
|
||||
"Monitor, stop, delete, and share execution runs; "
|
||||
"manage human-in-the-loop reviews"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "credits",
|
||||
@@ -73,11 +87,11 @@ v2_app = FastAPI(
|
||||
},
|
||||
{
|
||||
"name": "integrations",
|
||||
"description": "Manage OAuth credentials for external services",
|
||||
"description": "List, create, and delete integration credentials",
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"description": "Upload files for agent input",
|
||||
"description": "Upload, list, download, and delete workspace files",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ from .models import BlockInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
blocks_router = APIRouter()
|
||||
blocks_router = APIRouter(tags=["blocks"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -56,9 +56,10 @@ async def _get_cached_blocks() -> list[BlockInfo]:
|
||||
|
||||
@blocks_router.get(
|
||||
path="",
|
||||
summary="List blocks",
|
||||
summary="List available blocks",
|
||||
operation_id="listAvailableBlocks",
|
||||
)
|
||||
async def list_blocks(
|
||||
async def list_available_blocks(
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.READ_BLOCK)
|
||||
),
|
||||
|
||||
@@ -19,7 +19,7 @@ from .models import CreditBalance, CreditTransaction, CreditTransactionsResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
credits_router = APIRouter()
|
||||
credits_router = APIRouter(tags=["credits"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -30,6 +30,7 @@ credits_router = APIRouter()
|
||||
@credits_router.get(
|
||||
path="",
|
||||
summary="Get credit balance",
|
||||
operation_id="getCreditBalance",
|
||||
)
|
||||
async def get_balance(
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
@@ -45,7 +46,8 @@ async def get_balance(
|
||||
|
||||
@credits_router.get(
|
||||
path="/transactions",
|
||||
summary="Get transaction history",
|
||||
summary="Get credit transaction history",
|
||||
operation_id="listCreditTransactions",
|
||||
)
|
||||
async def get_transactions(
|
||||
page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
V2 External API - Files Endpoints
|
||||
|
||||
Provides file upload and download functionality.
|
||||
Provides file upload, download, listing, metadata, and deletion functionality.
|
||||
"""
|
||||
|
||||
import base64
|
||||
@@ -16,19 +16,30 @@ from starlette import status
|
||||
|
||||
from backend.api.external.middleware import require_permission
|
||||
from backend.data.auth.base import APIAuthorizationInfo
|
||||
from backend.data.workspace import get_workspace, get_workspace_file
|
||||
from backend.data.workspace import (
|
||||
count_workspace_files,
|
||||
get_workspace,
|
||||
get_workspace_file,
|
||||
list_workspace_files,
|
||||
soft_delete_workspace_file,
|
||||
)
|
||||
from backend.util.cloud_storage import get_cloud_storage_handler
|
||||
from backend.util.settings import Settings
|
||||
from backend.util.virus_scanner import scan_content_safe
|
||||
from backend.util.workspace_storage import get_workspace_storage
|
||||
|
||||
from .models import UploadFileResponse
|
||||
from .common import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
|
||||
from .models import (
|
||||
UploadWorkspaceFileResponse,
|
||||
WorkspaceFileInfo,
|
||||
WorkspaceFileListResponse,
|
||||
)
|
||||
from .rate_limit import file_upload_limiter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = Settings()
|
||||
|
||||
files_router = APIRouter()
|
||||
file_workspace_router = APIRouter(tags=["files"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -36,6 +47,125 @@ files_router = APIRouter()
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@file_workspace_router.get(
|
||||
path="",
|
||||
summary="List workspace files",
|
||||
operation_id="listWorkspaceFiles",
|
||||
)
|
||||
async def list_files(
|
||||
page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
|
||||
page_size: int = Query(
|
||||
default=DEFAULT_PAGE_SIZE,
|
||||
ge=1,
|
||||
le=MAX_PAGE_SIZE,
|
||||
description=f"Items per page (max {MAX_PAGE_SIZE})",
|
||||
),
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.DOWNLOAD_FILES)
|
||||
),
|
||||
) -> WorkspaceFileListResponse:
|
||||
"""List files in the user's workspace."""
|
||||
workspace = await get_workspace(auth.user_id)
|
||||
if workspace is None:
|
||||
return WorkspaceFileListResponse(
|
||||
files=[], page=page, page_size=page_size, total_count=0, total_pages=0
|
||||
)
|
||||
|
||||
total_count = await count_workspace_files(workspace.id)
|
||||
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 0
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
files = await list_workspace_files(
|
||||
workspace_id=workspace.id,
|
||||
limit=page_size,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return WorkspaceFileListResponse(
|
||||
files=[
|
||||
WorkspaceFileInfo(
|
||||
id=f.id,
|
||||
name=f.name,
|
||||
path=f.path,
|
||||
mime_type=f.mime_type,
|
||||
size_bytes=f.size_bytes,
|
||||
created_at=f.created_at,
|
||||
updated_at=f.updated_at,
|
||||
)
|
||||
for f in files
|
||||
],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_count=total_count,
|
||||
total_pages=total_pages,
|
||||
)
|
||||
|
||||
|
||||
@file_workspace_router.get(
|
||||
path="/{file_id}",
|
||||
summary="Get workspace file metadata",
|
||||
operation_id="getWorkspaceFileInfo",
|
||||
)
|
||||
async def get_file(
|
||||
file_id: str,
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.DOWNLOAD_FILES)
|
||||
),
|
||||
) -> WorkspaceFileInfo:
|
||||
"""Get metadata for a specific file in the user's workspace."""
|
||||
workspace = await get_workspace(auth.user_id)
|
||||
if workspace is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Workspace not found",
|
||||
)
|
||||
|
||||
file = await get_workspace_file(file_id, workspace.id)
|
||||
if file is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"File #{file_id} not found",
|
||||
)
|
||||
|
||||
return WorkspaceFileInfo(
|
||||
id=file.id,
|
||||
name=file.name,
|
||||
path=file.path,
|
||||
mime_type=file.mime_type,
|
||||
size_bytes=file.size_bytes,
|
||||
created_at=file.created_at,
|
||||
updated_at=file.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@file_workspace_router.delete(
|
||||
path="/{file_id}",
|
||||
summary="Delete file from workspace",
|
||||
operation_id="deleteWorkspaceFile",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_file(
|
||||
file_id: str,
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.UPLOAD_FILES)
|
||||
),
|
||||
) -> None:
|
||||
"""Soft-delete a file from the user's workspace."""
|
||||
workspace = await get_workspace(auth.user_id)
|
||||
if workspace is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Workspace not found",
|
||||
)
|
||||
|
||||
result = await soft_delete_workspace_file(file_id, workspace.id)
|
||||
if result is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"File #{file_id} not found",
|
||||
)
|
||||
|
||||
|
||||
def _create_file_size_error(size_bytes: int, max_size_mb: int) -> HTTPException:
|
||||
"""Create standardized file size error response."""
|
||||
return HTTPException(
|
||||
@@ -47,9 +177,10 @@ def _create_file_size_error(size_bytes: int, max_size_mb: int) -> HTTPException:
|
||||
)
|
||||
|
||||
|
||||
@files_router.post(
|
||||
@file_workspace_router.post(
|
||||
path="/upload",
|
||||
summary="Upload file",
|
||||
summary="Upload file to workspace",
|
||||
operation_id="uploadWorkspaceFile",
|
||||
)
|
||||
async def upload_file(
|
||||
file: UploadFile = File(...),
|
||||
@@ -59,7 +190,7 @@ async def upload_file(
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.UPLOAD_FILES)
|
||||
),
|
||||
) -> UploadFileResponse:
|
||||
) -> UploadWorkspaceFileResponse:
|
||||
"""
|
||||
Upload a file to cloud storage for use with agents.
|
||||
|
||||
@@ -98,7 +229,7 @@ async def upload_file(
|
||||
base64_content = base64.b64encode(content).decode("utf-8")
|
||||
data_uri = f"data:{content_type};base64,{base64_content}"
|
||||
|
||||
return UploadFileResponse(
|
||||
return UploadWorkspaceFileResponse(
|
||||
file_uri=data_uri,
|
||||
file_name=file_name,
|
||||
size=content_size,
|
||||
@@ -114,7 +245,7 @@ async def upload_file(
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
|
||||
return UploadFileResponse(
|
||||
return UploadWorkspaceFileResponse(
|
||||
file_uri=storage_path,
|
||||
file_name=file_name,
|
||||
size=content_size,
|
||||
@@ -140,9 +271,10 @@ def _sanitize_filename_for_header(filename: str) -> str:
|
||||
return f"attachment; filename*=UTF-8''{encoded}"
|
||||
|
||||
|
||||
@files_router.get(
|
||||
@file_workspace_router.get(
|
||||
path="/{file_id}/download",
|
||||
summary="Download file",
|
||||
summary="Download file from workspace",
|
||||
operation_id="getWorkspaceFileDownload",
|
||||
)
|
||||
async def download_file(
|
||||
file_id: str,
|
||||
|
||||
@@ -37,12 +37,13 @@ from .models import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
graphs_router = APIRouter()
|
||||
graphs_router = APIRouter(tags=["graphs"])
|
||||
|
||||
|
||||
@graphs_router.get(
|
||||
path="",
|
||||
summary="List graphs",
|
||||
operation_id="listGraphs",
|
||||
)
|
||||
async def list_graphs(
|
||||
page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
|
||||
@@ -75,6 +76,7 @@ async def list_graphs(
|
||||
@graphs_router.get(
|
||||
path="/{graph_id}",
|
||||
summary="Get graph details",
|
||||
operation_id="getGraphDetails",
|
||||
)
|
||||
async def get_graph(
|
||||
graph_id: str,
|
||||
@@ -109,6 +111,7 @@ async def get_graph(
|
||||
@graphs_router.post(
|
||||
path="",
|
||||
summary="Create graph",
|
||||
operation_id="createGraph",
|
||||
)
|
||||
async def create_graph(
|
||||
create_graph: GraphCreateRequest,
|
||||
@@ -134,7 +137,8 @@ async def create_graph(
|
||||
|
||||
@graphs_router.put(
|
||||
path="/{graph_id}",
|
||||
summary="Update graph",
|
||||
summary="Update graph by creating a new version",
|
||||
operation_id="updateGraphCreateVersion",
|
||||
)
|
||||
async def update_graph(
|
||||
graph_id: str,
|
||||
@@ -229,6 +233,7 @@ async def update_graph(
|
||||
@graphs_router.get(
|
||||
path="/{graph_id}/versions",
|
||||
summary="List graph versions",
|
||||
operation_id="listGraphVersions",
|
||||
)
|
||||
async def list_graph_versions(
|
||||
graph_id: str,
|
||||
@@ -249,6 +254,7 @@ async def list_graph_versions(
|
||||
@graphs_router.put(
|
||||
path="/{graph_id}/versions/active",
|
||||
summary="Set active graph version",
|
||||
operation_id="updateGraphSetActiveVersion",
|
||||
)
|
||||
async def set_active_version(
|
||||
graph_id: str,
|
||||
@@ -299,6 +305,7 @@ async def set_active_version(
|
||||
@graphs_router.patch(
|
||||
path="/{graph_id}/settings",
|
||||
summary="Update graph settings",
|
||||
operation_id="updateGraphSettings",
|
||||
)
|
||||
async def update_graph_settings(
|
||||
graph_id: str,
|
||||
@@ -332,6 +339,7 @@ async def update_graph_settings(
|
||||
@graphs_router.get(
|
||||
path="/{graph_id}/library-agent",
|
||||
summary="Get library agent for graph",
|
||||
operation_id="getLibraryAgentForGraph",
|
||||
)
|
||||
async def get_library_agent_by_graph(
|
||||
graph_id: str,
|
||||
@@ -354,7 +362,8 @@ async def get_library_agent_by_graph(
|
||||
|
||||
@graphs_router.get(
|
||||
path="/{graph_id}/blocks",
|
||||
summary="List graph blocks",
|
||||
summary="List blocks used in a graph",
|
||||
operation_id="listBlocksInGraph",
|
||||
)
|
||||
async def list_graph_blocks(
|
||||
graph_id: str,
|
||||
@@ -395,6 +404,7 @@ async def list_graph_blocks(
|
||||
@graphs_router.get(
|
||||
path="/{graph_id}/credentials",
|
||||
summary="Get graph credentials",
|
||||
operation_id="getCredentialRequirementsForGraph",
|
||||
)
|
||||
async def list_graph_credential_requirements(
|
||||
graph_id: str,
|
||||
|
||||
@@ -8,6 +8,6 @@ from fastapi import APIRouter
|
||||
|
||||
from .credentials import credentials_router
|
||||
|
||||
integrations_router = APIRouter()
|
||||
integrations_router = APIRouter(tags=["integrations"])
|
||||
|
||||
integrations_router.include_router(credentials_router)
|
||||
|
||||
@@ -27,7 +27,8 @@ credentials_router = APIRouter()
|
||||
|
||||
@credentials_router.get(
|
||||
path="/credentials",
|
||||
summary="List credentials",
|
||||
summary="List integration credentials",
|
||||
operation_id="listIntegrationCredentials",
|
||||
)
|
||||
async def list_credentials(
|
||||
provider: Optional[str] = Query(
|
||||
@@ -51,7 +52,8 @@ async def list_credentials(
|
||||
|
||||
@credentials_router.post(
|
||||
path="/credentials",
|
||||
summary="Create API key credential",
|
||||
summary="Add integration API key credential",
|
||||
operation_id="addIntegrationAPIKeyCredential",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_credential(
|
||||
@@ -61,7 +63,7 @@ async def create_credential(
|
||||
),
|
||||
) -> CredentialInfo:
|
||||
"""
|
||||
Create a new API key credential.
|
||||
Create a new integration credential.
|
||||
|
||||
Only API key credentials can be created via the external API.
|
||||
OAuth credentials must be set up through the web UI.
|
||||
@@ -79,7 +81,8 @@ async def create_credential(
|
||||
|
||||
@credentials_router.delete(
|
||||
path="/credentials/{credential_id}",
|
||||
summary="Delete credential",
|
||||
summary="Delete integration credential",
|
||||
operation_id="deleteIntegrationCredential",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_credential(
|
||||
|
||||
@@ -28,7 +28,7 @@ from ..rate_limit import execute_limiter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
agents_router = APIRouter()
|
||||
agents_router = APIRouter(tags=["library"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -39,6 +39,7 @@ agents_router = APIRouter()
|
||||
@agents_router.get(
|
||||
path="/agents",
|
||||
summary="List library agents",
|
||||
operation_id="listLibraryAgents",
|
||||
)
|
||||
async def list_library_agents(
|
||||
published: Optional[bool] = Query(
|
||||
@@ -81,6 +82,7 @@ async def list_library_agents(
|
||||
@agents_router.get(
|
||||
path="/agents/{agent_id}",
|
||||
summary="Get library agent",
|
||||
operation_id="getLibraryAgent",
|
||||
)
|
||||
async def get_library_agent(
|
||||
agent_id: str,
|
||||
@@ -99,6 +101,7 @@ async def get_library_agent(
|
||||
@agents_router.patch(
|
||||
path="/agents/{agent_id}",
|
||||
summary="Update library agent",
|
||||
operation_id="updateLibraryAgent",
|
||||
)
|
||||
async def update_library_agent(
|
||||
request: LibraryAgentUpdateRequest,
|
||||
@@ -123,6 +126,7 @@ async def update_library_agent(
|
||||
@agents_router.delete(
|
||||
path="/agents/{agent_id}",
|
||||
summary="Delete library agent",
|
||||
operation_id="deleteLibraryAgent",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_library_agent(
|
||||
@@ -141,6 +145,7 @@ async def delete_library_agent(
|
||||
@agents_router.post(
|
||||
path="/agents/{agent_id}/fork",
|
||||
summary="Fork library agent",
|
||||
operation_id="forkLibraryAgent",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def fork_library_agent(
|
||||
@@ -149,7 +154,12 @@ async def fork_library_agent(
|
||||
require_permission(APIKeyPermission.WRITE_LIBRARY)
|
||||
),
|
||||
) -> LibraryAgent:
|
||||
"""Fork (clone) a library agent."""
|
||||
"""Fork (clone) a library agent.
|
||||
|
||||
Creates a deep copy of the agent's underlying graph and all its nodes,
|
||||
assigning new IDs. The cloned graph is added to the user's library as
|
||||
an independent agent that can be modified without affecting the original.
|
||||
"""
|
||||
forked = await library_db.fork_library_agent(
|
||||
library_agent_id=agent_id,
|
||||
user_id=auth.user_id,
|
||||
@@ -160,6 +170,7 @@ async def fork_library_agent(
|
||||
@agents_router.post(
|
||||
path="/agents/{agent_id}/runs",
|
||||
summary="Execute library agent",
|
||||
operation_id="executeLibraryAgent",
|
||||
)
|
||||
async def execute_agent(
|
||||
request: AgentRunRequest,
|
||||
@@ -186,25 +197,20 @@ async def execute_agent(
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
|
||||
try:
|
||||
result = await execution_utils.add_graph_execution(
|
||||
graph_id=library_agent.graph_id,
|
||||
user_id=auth.user_id,
|
||||
inputs=request.inputs,
|
||||
graph_version=library_agent.graph_version,
|
||||
graph_credentials_inputs=request.credentials_inputs,
|
||||
)
|
||||
|
||||
return AgentGraphRun.from_internal(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to execute agent: {e}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
result = await execution_utils.add_graph_execution(
|
||||
graph_id=library_agent.graph_id,
|
||||
user_id=auth.user_id,
|
||||
inputs=request.inputs,
|
||||
graph_version=library_agent.graph_version,
|
||||
graph_credentials_inputs=request.credentials_inputs,
|
||||
)
|
||||
return AgentGraphRun.from_internal(result)
|
||||
|
||||
|
||||
@agents_router.get(
|
||||
path="/agents/{agent_id}/credentials",
|
||||
summary="Get agent credentials",
|
||||
summary="Get library agent credential requirements",
|
||||
operation_id="getCredentialRequirementsForLibraryAgent",
|
||||
)
|
||||
async def list_agent_credential_requirements(
|
||||
agent_id: str,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Security
|
||||
from fastapi import APIRouter, Query, Security
|
||||
from prisma.enums import APIKeyPermission
|
||||
from starlette import status
|
||||
|
||||
@@ -23,12 +23,13 @@ from ..models import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
folders_router = APIRouter()
|
||||
folders_router = APIRouter(tags=["library"])
|
||||
|
||||
|
||||
@folders_router.get(
|
||||
path="/folders",
|
||||
summary="List folders",
|
||||
summary="List folders in library",
|
||||
operation_id="listLibraryFolders",
|
||||
)
|
||||
async def list_folders(
|
||||
parent_id: Optional[str] = Query(
|
||||
@@ -51,7 +52,8 @@ async def list_folders(
|
||||
|
||||
@folders_router.get(
|
||||
path="/folders/tree",
|
||||
summary="Get folder tree",
|
||||
summary="Get library folder tree",
|
||||
operation_id="getLibraryFolderTree",
|
||||
)
|
||||
async def get_folder_tree(
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
@@ -68,7 +70,8 @@ async def get_folder_tree(
|
||||
|
||||
@folders_router.get(
|
||||
path="/folders/{folder_id}",
|
||||
summary="Get folder",
|
||||
summary="Get folder in library",
|
||||
operation_id="getLibraryFolder",
|
||||
)
|
||||
async def get_folder(
|
||||
folder_id: str,
|
||||
@@ -77,23 +80,17 @@ async def get_folder(
|
||||
),
|
||||
) -> LibraryFolder:
|
||||
"""Get details of a specific folder."""
|
||||
try:
|
||||
folder = await library_db.get_folder(
|
||||
folder_id=folder_id,
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Folder #{folder_id} not found",
|
||||
)
|
||||
|
||||
folder = await library_db.get_folder(
|
||||
folder_id=folder_id,
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
return LibraryFolder.from_internal(folder)
|
||||
|
||||
|
||||
@folders_router.post(
|
||||
path="/folders",
|
||||
summary="Create folder",
|
||||
summary="Create folder in library",
|
||||
operation_id="createLibraryFolder",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_folder(
|
||||
@@ -103,23 +100,20 @@ async def create_folder(
|
||||
),
|
||||
) -> LibraryFolder:
|
||||
"""Create a new folder in the user's library."""
|
||||
try:
|
||||
folder = await library_db.create_folder(
|
||||
user_id=auth.user_id,
|
||||
name=request.name,
|
||||
parent_id=request.parent_id,
|
||||
icon=request.icon,
|
||||
color=request.color,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
folder = await library_db.create_folder(
|
||||
user_id=auth.user_id,
|
||||
name=request.name,
|
||||
parent_id=request.parent_id,
|
||||
icon=request.icon,
|
||||
color=request.color,
|
||||
)
|
||||
return LibraryFolder.from_internal(folder)
|
||||
|
||||
|
||||
@folders_router.patch(
|
||||
path="/folders/{folder_id}",
|
||||
summary="Update folder",
|
||||
summary="Update folder in library",
|
||||
operation_id="updateLibraryFolder",
|
||||
)
|
||||
async def update_folder(
|
||||
request: LibraryFolderUpdateRequest,
|
||||
@@ -129,23 +123,20 @@ async def update_folder(
|
||||
),
|
||||
) -> LibraryFolder:
|
||||
"""Update properties of a folder."""
|
||||
try:
|
||||
folder = await library_db.update_folder(
|
||||
folder_id=folder_id,
|
||||
user_id=auth.user_id,
|
||||
name=request.name,
|
||||
icon=request.icon,
|
||||
color=request.color,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
folder = await library_db.update_folder(
|
||||
folder_id=folder_id,
|
||||
user_id=auth.user_id,
|
||||
name=request.name,
|
||||
icon=request.icon,
|
||||
color=request.color,
|
||||
)
|
||||
return LibraryFolder.from_internal(folder)
|
||||
|
||||
|
||||
@folders_router.post(
|
||||
path="/folders/{folder_id}/move",
|
||||
summary="Move folder",
|
||||
summary="Move folder in library",
|
||||
operation_id="moveLibraryFolder",
|
||||
)
|
||||
async def move_folder(
|
||||
request: LibraryFolderMoveRequest,
|
||||
@@ -155,21 +146,18 @@ async def move_folder(
|
||||
),
|
||||
) -> LibraryFolder:
|
||||
"""Move a folder to a new parent. Set target_parent_id to null to move to root."""
|
||||
try:
|
||||
folder = await library_db.move_folder(
|
||||
folder_id=folder_id,
|
||||
user_id=auth.user_id,
|
||||
target_parent_id=request.target_parent_id,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
folder = await library_db.move_folder(
|
||||
folder_id=folder_id,
|
||||
user_id=auth.user_id,
|
||||
target_parent_id=request.target_parent_id,
|
||||
)
|
||||
return LibraryFolder.from_internal(folder)
|
||||
|
||||
|
||||
@folders_router.delete(
|
||||
path="/folders/{folder_id}",
|
||||
summary="Delete folder",
|
||||
summary="Delete folder in library",
|
||||
operation_id="deleteLibraryFolder",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_folder(
|
||||
@@ -178,14 +166,10 @@ async def delete_folder(
|
||||
require_permission(APIKeyPermission.WRITE_LIBRARY)
|
||||
),
|
||||
) -> None:
|
||||
"""Delete a folder. Agents in the folder will be moved to root. Subfolders are also deleted."""
|
||||
try:
|
||||
await library_db.delete_folder(
|
||||
folder_id=folder_id,
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Folder #{folder_id} not found",
|
||||
)
|
||||
"""
|
||||
Delete a folder and its subfolders. Agents in this folder will be moved to root.
|
||||
"""
|
||||
await library_db.delete_folder(
|
||||
folder_id=folder_id,
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
|
||||
@@ -35,12 +35,13 @@ from ..rate_limit import execute_limiter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
presets_router = APIRouter()
|
||||
presets_router = APIRouter(tags=["library", "presets"])
|
||||
|
||||
|
||||
@presets_router.get(
|
||||
path="/presets",
|
||||
summary="List presets",
|
||||
summary="List agent execution presets",
|
||||
operation_id="listAgentRunPresets",
|
||||
)
|
||||
async def list_presets(
|
||||
graph_id: Optional[str] = Query(default=None, description="Filter by graph ID"),
|
||||
@@ -74,7 +75,8 @@ async def list_presets(
|
||||
|
||||
@presets_router.get(
|
||||
path="/presets/{preset_id}",
|
||||
summary="Get preset",
|
||||
summary="Get agent execution preset",
|
||||
operation_id="getAgentRunPreset",
|
||||
)
|
||||
async def get_preset(
|
||||
preset_id: str,
|
||||
@@ -98,7 +100,8 @@ async def get_preset(
|
||||
|
||||
@presets_router.post(
|
||||
path="/presets",
|
||||
summary="Create preset",
|
||||
summary="Create agent execution preset",
|
||||
operation_id="createAgentRunPreset",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_preset(
|
||||
@@ -118,20 +121,17 @@ async def create_preset(
|
||||
is_active=request.is_active,
|
||||
)
|
||||
|
||||
try:
|
||||
preset = await library_db.create_preset(
|
||||
user_id=auth.user_id,
|
||||
preset=creatable,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
preset = await library_db.create_preset(
|
||||
user_id=auth.user_id,
|
||||
preset=creatable,
|
||||
)
|
||||
return AgentPreset.from_internal(preset)
|
||||
|
||||
|
||||
@presets_router.post(
|
||||
path="/presets/setup-trigger",
|
||||
summary="Setup triggered preset",
|
||||
operation_id="setupAgentRunTrigger",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def setup_trigger(
|
||||
@@ -140,7 +140,14 @@ async def setup_trigger(
|
||||
require_permission(APIKeyPermission.WRITE_LIBRARY)
|
||||
),
|
||||
) -> AgentPreset:
|
||||
"""Create a preset with a webhook trigger for automatic execution."""
|
||||
"""
|
||||
Create a preset with a webhook trigger for automatic execution.
|
||||
|
||||
The agent's `trigger_setup_info` describes the required trigger configuration
|
||||
schema and credentials. Use it to populate `trigger_config` and
|
||||
`agent_credentials`.
|
||||
"""
|
||||
# Use internal trigger setup endpoint to avoid logic duplication:
|
||||
from backend.api.features.library.routes.presets import (
|
||||
setup_trigger as _internal_setup_trigger,
|
||||
)
|
||||
@@ -154,22 +161,17 @@ async def setup_trigger(
|
||||
agent_credentials=request.agent_credentials,
|
||||
)
|
||||
|
||||
try:
|
||||
preset = await _internal_setup_trigger(
|
||||
params=internal_request,
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
preset = await _internal_setup_trigger(
|
||||
params=internal_request,
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
return AgentPreset.from_internal(preset)
|
||||
|
||||
|
||||
@presets_router.patch(
|
||||
path="/presets/{preset_id}",
|
||||
summary="Update preset",
|
||||
operation_id="updateAgentRunPreset",
|
||||
summary="Update agent execution preset",
|
||||
)
|
||||
async def update_preset(
|
||||
request: AgentPresetUpdateRequest,
|
||||
@@ -179,25 +181,22 @@ async def update_preset(
|
||||
),
|
||||
) -> AgentPreset:
|
||||
"""Update properties of a preset. Only provided fields will be updated."""
|
||||
try:
|
||||
preset = await library_db.update_preset(
|
||||
user_id=auth.user_id,
|
||||
preset_id=preset_id,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
inputs=request.inputs,
|
||||
credentials=request.credentials,
|
||||
is_active=request.is_active,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
preset = await library_db.update_preset(
|
||||
user_id=auth.user_id,
|
||||
preset_id=preset_id,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
inputs=request.inputs,
|
||||
credentials=request.credentials,
|
||||
is_active=request.is_active,
|
||||
)
|
||||
return AgentPreset.from_internal(preset)
|
||||
|
||||
|
||||
@presets_router.delete(
|
||||
path="/presets/{preset_id}",
|
||||
summary="Delete preset",
|
||||
summary="Delete agent execution preset",
|
||||
operation_id="deleteAgentRunPreset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_preset(
|
||||
@@ -215,7 +214,8 @@ async def delete_preset(
|
||||
|
||||
@presets_router.post(
|
||||
path="/presets/{preset_id}/execute",
|
||||
summary="Execute preset",
|
||||
summary="Execute agent preset",
|
||||
operation_id="executeAgentRunPreset",
|
||||
)
|
||||
async def execute_preset(
|
||||
preset_id: str,
|
||||
@@ -251,16 +251,12 @@ async def execute_preset(
|
||||
merged_inputs = {**preset.inputs, **request.inputs}
|
||||
merged_credentials = {**preset.credentials, **request.credentials_inputs}
|
||||
|
||||
try:
|
||||
result = await execution_utils.add_graph_execution(
|
||||
graph_id=preset.graph_id,
|
||||
user_id=auth.user_id,
|
||||
inputs=merged_inputs,
|
||||
graph_version=preset.graph_version,
|
||||
graph_credentials_inputs=merged_credentials,
|
||||
preset_id=preset_id,
|
||||
)
|
||||
return AgentGraphRun.from_internal(result)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to execute preset: {e}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
result = await execution_utils.add_graph_execution(
|
||||
graph_id=preset.graph_id,
|
||||
user_id=auth.user_id,
|
||||
inputs=merged_inputs,
|
||||
graph_version=preset.graph_version,
|
||||
graph_credentials_inputs=merged_credentials,
|
||||
preset_id=preset_id,
|
||||
)
|
||||
return AgentGraphRun.from_internal(result)
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import Literal, Optional
|
||||
|
||||
from fastapi import APIRouter, File, HTTPException, Path, Query, Security, UploadFile
|
||||
from prisma.enums import APIKeyPermission
|
||||
from prisma.enums import ContentType as SearchContentType
|
||||
from starlette import status
|
||||
|
||||
from backend.api.external.middleware import require_auth, require_permission
|
||||
@@ -21,7 +20,6 @@ from backend.api.features.store.db import (
|
||||
StoreAgentsSortOptions,
|
||||
StoreCreatorsSortOptions,
|
||||
)
|
||||
from backend.api.features.store.hybrid_search import unified_hybrid_search
|
||||
from backend.data.auth.base import APIAuthorizationInfo
|
||||
from backend.util.virus_scanner import scan_content_safe
|
||||
|
||||
@@ -38,16 +36,14 @@ from .models import (
|
||||
MarketplaceCreatorDetails,
|
||||
MarketplaceCreatorsResponse,
|
||||
MarketplaceMediaUploadResponse,
|
||||
MarketplaceSearchResponse,
|
||||
MarketplaceSearchResult,
|
||||
MarketplaceUserProfile,
|
||||
MarketplaceUserProfileUpdateRequest,
|
||||
)
|
||||
from .rate_limit import media_upload_limiter, search_limiter
|
||||
from .rate_limit import media_upload_limiter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
marketplace_router = APIRouter()
|
||||
marketplace_router = APIRouter(tags=["marketplace"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -57,7 +53,8 @@ marketplace_router = APIRouter()
|
||||
|
||||
@marketplace_router.get(
|
||||
path="/agents",
|
||||
summary="List marketplace agents",
|
||||
summary="List or search marketplace agents",
|
||||
operation_id="listMarketplaceAgents",
|
||||
)
|
||||
async def list_agents(
|
||||
featured: bool = Query(
|
||||
@@ -100,7 +97,8 @@ async def list_agents(
|
||||
|
||||
@marketplace_router.get(
|
||||
path="/agents/by-version/{version_id}",
|
||||
summary="Get agent by version ID",
|
||||
summary="Get marketplace agent by version ID",
|
||||
operation_id="getMarketplaceAgentByListingVersion",
|
||||
)
|
||||
async def get_agent_by_version(
|
||||
version_id: str,
|
||||
@@ -113,7 +111,8 @@ async def get_agent_by_version(
|
||||
|
||||
@marketplace_router.get(
|
||||
path="/agents/{username}/{agent_name}",
|
||||
summary="Get agent details",
|
||||
summary="Get marketplace agent details",
|
||||
operation_id="getMarketplaceAgent",
|
||||
)
|
||||
async def get_agent_details(
|
||||
username: str,
|
||||
@@ -133,7 +132,8 @@ async def get_agent_details(
|
||||
|
||||
@marketplace_router.post(
|
||||
path="/agents/{username}/{agent_name}/add-to-library",
|
||||
summary="Add agent to library",
|
||||
summary="Add marketplace agent to library",
|
||||
operation_id="addMarketplaceAgentToLibrary",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def add_agent_to_library(
|
||||
@@ -161,63 +161,6 @@ async def add_agent_to_library(
|
||||
return LibraryAgent.from_internal(agent)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Search
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@marketplace_router.get(
|
||||
path="/search",
|
||||
summary="Search marketplace",
|
||||
)
|
||||
async def search_marketplace(
|
||||
query: str = Query(description="Search query"),
|
||||
content_types: Optional[list[SearchContentType]] = Query(
|
||||
default=None, description="Content types to filter by"
|
||||
),
|
||||
category: Optional[str] = Query(default=None, description="Filter by category"),
|
||||
page: int = Query(ge=1, default=1),
|
||||
page_size: int = Query(ge=1, le=MAX_PAGE_SIZE, default=DEFAULT_PAGE_SIZE),
|
||||
auth: APIAuthorizationInfo = Security(require_auth),
|
||||
) -> MarketplaceSearchResponse:
|
||||
"""
|
||||
Search the marketplace using hybrid search (literal + semantic).
|
||||
|
||||
Searches across agents, blocks, and documentation. Results are ranked
|
||||
by a combination of keyword matching and semantic similarity.
|
||||
"""
|
||||
search_limiter.check(auth.user_id)
|
||||
|
||||
results, total_count = await unified_hybrid_search(
|
||||
query=query,
|
||||
content_types=content_types,
|
||||
category=category,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
|
||||
total_pages = max(1, (total_count + page_size - 1) // page_size)
|
||||
|
||||
return MarketplaceSearchResponse(
|
||||
results=[
|
||||
MarketplaceSearchResult(
|
||||
content_type=r.get("content_type", ""),
|
||||
content_id=r.get("content_id", ""),
|
||||
searchable_text=r.get("searchable_text", ""),
|
||||
metadata=r.get("metadata"),
|
||||
updated_at=r.get("updated_at"),
|
||||
combined_score=r.get("combined_score"),
|
||||
)
|
||||
for r in results
|
||||
],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_count=total_count,
|
||||
total_pages=total_pages,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Creators
|
||||
# ============================================================================
|
||||
@@ -226,6 +169,7 @@ async def search_marketplace(
|
||||
@marketplace_router.get(
|
||||
path="/creators",
|
||||
summary="List marketplace creators",
|
||||
operation_id="listMarketplaceCreators",
|
||||
)
|
||||
async def list_creators(
|
||||
featured: bool = Query(
|
||||
@@ -261,13 +205,14 @@ async def list_creators(
|
||||
|
||||
@marketplace_router.get(
|
||||
path="/creators/{username}",
|
||||
summary="Get creator details",
|
||||
summary="Get marketplace creator details",
|
||||
operation_id="getMarketplaceCreator",
|
||||
)
|
||||
async def get_creator_details(
|
||||
username: str,
|
||||
auth: APIAuthorizationInfo = Security(require_auth),
|
||||
) -> MarketplaceCreatorDetails:
|
||||
"""Get details on a marketplace creator."""
|
||||
"""Get a marketplace creator's profile w/ stats."""
|
||||
username = urllib.parse.unquote(username).lower()
|
||||
creator = await store_cache._get_cached_creator_details(username=username)
|
||||
return MarketplaceCreatorDetails.from_internal(creator)
|
||||
@@ -280,14 +225,15 @@ async def get_creator_details(
|
||||
|
||||
@marketplace_router.get(
|
||||
path="/profile",
|
||||
summary="Get my profile",
|
||||
summary="Get my marketplace profile",
|
||||
operation_id="getMarketplaceMyProfile",
|
||||
)
|
||||
async def get_profile(
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.READ_STORE)
|
||||
),
|
||||
) -> MarketplaceCreatorDetails:
|
||||
"""Get the authenticated user's marketplace profile."""
|
||||
"""Get the authenticated user's marketplace profile w/ creator stats."""
|
||||
profile = await store_db.get_user_profile(auth.user_id)
|
||||
if not profile:
|
||||
raise HTTPException(
|
||||
@@ -301,7 +247,8 @@ async def get_profile(
|
||||
|
||||
@marketplace_router.patch(
|
||||
path="/profile",
|
||||
summary="Update my profile",
|
||||
summary="Update my marketplace profile",
|
||||
operation_id="updateMarketplaceMyProfile",
|
||||
)
|
||||
async def update_profile(
|
||||
request: MarketplaceUserProfileUpdateRequest,
|
||||
@@ -331,7 +278,8 @@ async def update_profile(
|
||||
|
||||
@marketplace_router.get(
|
||||
path="/submissions",
|
||||
summary="List my submissions",
|
||||
summary="List my marketplace submissions",
|
||||
operation_id="listMarketplaceSubmissions",
|
||||
)
|
||||
async def list_submissions(
|
||||
page: int = Query(ge=1, default=1),
|
||||
@@ -360,7 +308,8 @@ async def list_submissions(
|
||||
|
||||
@marketplace_router.post(
|
||||
path="/submissions",
|
||||
summary="Create submission",
|
||||
summary="Create marketplace submission",
|
||||
operation_id="createMarketplaceSubmission",
|
||||
)
|
||||
async def create_submission(
|
||||
request: MarketplaceAgentSubmissionCreateRequest,
|
||||
@@ -391,7 +340,8 @@ async def create_submission(
|
||||
|
||||
@marketplace_router.put(
|
||||
path="/submissions/{version_id}",
|
||||
summary="Edit submission",
|
||||
summary="Edit marketplace submission",
|
||||
operation_id="updateMarketplaceSubmission",
|
||||
)
|
||||
async def edit_submission(
|
||||
request: MarketplaceAgentSubmissionEditRequest,
|
||||
@@ -423,25 +373,26 @@ async def edit_submission(
|
||||
|
||||
|
||||
@marketplace_router.delete(
|
||||
path="/submissions/{submission_id}",
|
||||
summary="Delete submission",
|
||||
path="/submissions/{version_id}",
|
||||
summary="Delete marketplace submission",
|
||||
operation_id="deleteMarketplaceSubmission",
|
||||
)
|
||||
async def delete_submission(
|
||||
submission_id: str,
|
||||
version_id: str,
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.WRITE_STORE)
|
||||
),
|
||||
) -> None:
|
||||
"""Delete a marketplace listing submission."""
|
||||
"""Delete a marketplace listing submission. Approved listings can not be deleted."""
|
||||
success = await store_db.delete_store_submission(
|
||||
user_id=auth.user_id,
|
||||
submission_id=submission_id,
|
||||
store_listing_version_id=version_id,
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Submission #{submission_id} not found",
|
||||
detail=f"Submission #{version_id} not found",
|
||||
)
|
||||
|
||||
|
||||
@@ -452,7 +403,8 @@ async def delete_submission(
|
||||
|
||||
@marketplace_router.post(
|
||||
path="/submissions/media",
|
||||
summary="Upload submission media",
|
||||
summary="Upload marketplace submission media",
|
||||
operation_id="uploadMarketplaceSubmissionMedia",
|
||||
)
|
||||
async def upload_submission_media(
|
||||
file: UploadFile = File(...),
|
||||
|
||||
@@ -386,6 +386,26 @@ class AgentRunScheduleListResponse(PaginatedResponse):
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TriggerSetupInfo(BaseModel):
|
||||
"""
|
||||
Trigger configuration requirements for agents that support webhook triggers.
|
||||
|
||||
Use `config_schema` and `credentials_input_name` to populate the
|
||||
`trigger_config` and `agent_credentials` fields when calling
|
||||
``POST /library/presets/setup-trigger``.
|
||||
"""
|
||||
|
||||
provider: str = Field(description="Trigger provider (e.g. 'github')")
|
||||
config_schema: dict[str, Any] = Field(
|
||||
description="JSON Schema for the trigger block's input"
|
||||
)
|
||||
credentials_input_name: Optional[str] = Field(
|
||||
description=(
|
||||
"Name of the credentials input field, if the trigger requires credentials"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class LibraryAgent(BaseModel):
|
||||
"""An agent in the user's library."""
|
||||
|
||||
@@ -401,11 +421,24 @@ class LibraryAgent(BaseModel):
|
||||
creator_name: str
|
||||
input_schema: dict[str, Any] = Field(description="Input schema for the agent")
|
||||
output_schema: dict[str, Any] = Field(description="Output schema for the agent")
|
||||
trigger_setup_info: Optional[TriggerSetupInfo] = Field(
|
||||
default=None,
|
||||
description="Trigger configuration requirements; "
|
||||
"present if the agent has a webhook trigger input",
|
||||
)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@classmethod
|
||||
def from_internal(cls, agent: _LibraryAgent) -> Self:
|
||||
trigger_info = None
|
||||
if agent.trigger_setup_info:
|
||||
trigger_info = TriggerSetupInfo(
|
||||
provider=agent.trigger_setup_info.provider,
|
||||
config_schema=agent.trigger_setup_info.config_schema,
|
||||
credentials_input_name=agent.trigger_setup_info.credentials_input_name,
|
||||
)
|
||||
|
||||
return cls(
|
||||
id=agent.id,
|
||||
graph_id=agent.graph_id,
|
||||
@@ -419,6 +452,7 @@ class LibraryAgent(BaseModel):
|
||||
creator_name=agent.creator_name,
|
||||
input_schema=agent.input_schema,
|
||||
output_schema=agent.output_schema,
|
||||
trigger_setup_info=trigger_info,
|
||||
created_at=agent.created_at,
|
||||
updated_at=agent.updated_at,
|
||||
)
|
||||
@@ -909,12 +943,12 @@ class CredentialRequirementsResponse(BaseModel):
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# File Models
|
||||
# File Workspace Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class UploadFileResponse(BaseModel):
|
||||
"""Response after uploading a file."""
|
||||
class UploadWorkspaceFileResponse(BaseModel):
|
||||
"""Response after uploading a file to the user's workspace."""
|
||||
|
||||
file_uri: str = Field(description="URI to reference the uploaded file in agents")
|
||||
file_name: str
|
||||
@@ -923,6 +957,24 @@ class UploadFileResponse(BaseModel):
|
||||
expires_in_hours: int
|
||||
|
||||
|
||||
class WorkspaceFileInfo(BaseModel):
|
||||
"""Metadata for a file in the user's workspace."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
path: str
|
||||
mime_type: str
|
||||
size_bytes: int = Field(description="File size in bytes")
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class WorkspaceFileListResponse(PaginatedResponse):
|
||||
"""Response for listing workspace files."""
|
||||
|
||||
files: list[WorkspaceFileInfo]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Marketplace Models
|
||||
# ============================================================================
|
||||
|
||||
@@ -8,28 +8,26 @@ from fastapi import APIRouter
|
||||
|
||||
from .blocks import blocks_router
|
||||
from .credits import credits_router
|
||||
from .files import files_router
|
||||
from .files import file_workspace_router
|
||||
from .graphs import graphs_router
|
||||
from .integrations import integrations_router
|
||||
from .library import library_router
|
||||
from .marketplace import marketplace_router
|
||||
from .runs import runs_router
|
||||
from .schedules import graph_schedules_router, schedules_router
|
||||
from .search import search_router
|
||||
|
||||
v2_router = APIRouter()
|
||||
|
||||
# Include all sub-routers
|
||||
v2_router.include_router(graphs_router, prefix="/graphs", tags=["graphs"])
|
||||
v2_router.include_router(graph_schedules_router, prefix="/graphs", tags=["schedules"])
|
||||
v2_router.include_router(schedules_router, prefix="/schedules", tags=["schedules"])
|
||||
v2_router.include_router(blocks_router, prefix="/blocks", tags=["blocks"])
|
||||
v2_router.include_router(
|
||||
marketplace_router, prefix="/marketplace", tags=["marketplace"]
|
||||
)
|
||||
v2_router.include_router(library_router, prefix="/library", tags=["library"])
|
||||
v2_router.include_router(runs_router, prefix="/runs", tags=["runs"])
|
||||
v2_router.include_router(credits_router, prefix="/credits", tags=["credits"])
|
||||
v2_router.include_router(
|
||||
integrations_router, prefix="/integrations", tags=["integrations"]
|
||||
)
|
||||
v2_router.include_router(files_router, prefix="/files", tags=["files"])
|
||||
v2_router.include_router(blocks_router, prefix="/blocks")
|
||||
v2_router.include_router(credits_router, prefix="/credits")
|
||||
v2_router.include_router(file_workspace_router, prefix="/files")
|
||||
v2_router.include_router(graph_schedules_router, prefix="/graphs")
|
||||
v2_router.include_router(graphs_router, prefix="/graphs")
|
||||
v2_router.include_router(integrations_router, prefix="/integrations")
|
||||
v2_router.include_router(library_router, prefix="/library")
|
||||
v2_router.include_router(marketplace_router, prefix="/marketplace")
|
||||
v2_router.include_router(runs_router, prefix="/runs")
|
||||
v2_router.include_router(schedules_router, prefix="/schedules")
|
||||
v2_router.include_router(search_router, prefix="/search")
|
||||
|
||||
@@ -9,7 +9,7 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Security
|
||||
from fastapi import APIRouter, HTTPException, Path, Query, Security
|
||||
from prisma.enums import APIKeyPermission, ReviewStatus
|
||||
from pydantic import JsonValue
|
||||
from starlette import status
|
||||
@@ -36,7 +36,7 @@ from .models import (
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = Settings()
|
||||
|
||||
runs_router = APIRouter()
|
||||
runs_router = APIRouter(tags=["runs"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -46,7 +46,8 @@ runs_router = APIRouter()
|
||||
|
||||
@runs_router.get(
|
||||
path="",
|
||||
summary="List runs",
|
||||
summary="List agent runs",
|
||||
operation_id="listAgentRuns",
|
||||
)
|
||||
async def list_runs(
|
||||
graph_id: Optional[str] = Query(default=None, description="Filter by graph ID"),
|
||||
@@ -80,10 +81,11 @@ async def list_runs(
|
||||
|
||||
@runs_router.get(
|
||||
path="/{run_id}",
|
||||
summary="Get run details",
|
||||
summary="Get agent run details",
|
||||
operation_id="getAgentRunDetails",
|
||||
)
|
||||
async def get_run(
|
||||
run_id: str,
|
||||
run_id: str = Path(description="Graph Execution ID"),
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.READ_RUN)
|
||||
),
|
||||
@@ -106,10 +108,11 @@ async def get_run(
|
||||
|
||||
@runs_router.post(
|
||||
path="/{run_id}/stop",
|
||||
summary="Stop run",
|
||||
summary="Stop agent run",
|
||||
operation_id="stopAgentRun",
|
||||
)
|
||||
async def stop_run(
|
||||
run_id: str,
|
||||
run_id: str = Path(description="Graph Execution ID"),
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.WRITE_RUN)
|
||||
),
|
||||
@@ -153,10 +156,11 @@ async def stop_run(
|
||||
|
||||
@runs_router.delete(
|
||||
path="/{run_id}",
|
||||
summary="Delete run",
|
||||
summary="Delete agent run",
|
||||
operation_id="deleteAgentRun",
|
||||
)
|
||||
async def delete_run(
|
||||
run_id: str,
|
||||
run_id: str = Path(description="Graph Execution ID"),
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.WRITE_RUN)
|
||||
),
|
||||
@@ -175,10 +179,11 @@ async def delete_run(
|
||||
|
||||
@runs_router.post(
|
||||
path="/{run_id}/share",
|
||||
summary="Enable run sharing",
|
||||
summary="Enable sharing for an agent run",
|
||||
operation_id="enableAgentRunShare",
|
||||
)
|
||||
async def enable_sharing(
|
||||
run_id: str,
|
||||
run_id: str = Path(description="Graph Execution ID"),
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.READ_RUN, APIKeyPermission.SHARE_RUN)
|
||||
),
|
||||
@@ -212,11 +217,12 @@ async def enable_sharing(
|
||||
|
||||
@runs_router.delete(
|
||||
path="/{run_id}/share",
|
||||
summary="Disable run sharing",
|
||||
summary="Disable sharing for an agent run",
|
||||
operation_id="disableAgentRunShare",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def disable_sharing(
|
||||
run_id: str,
|
||||
run_id: str = Path(description="Graph Execution ID"),
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.SHARE_RUN)
|
||||
),
|
||||
@@ -248,10 +254,13 @@ async def disable_sharing(
|
||||
|
||||
@runs_router.get(
|
||||
path="/reviews",
|
||||
summary="List run reviews",
|
||||
summary="List agent run human-in-the-loop reviews",
|
||||
operation_id="listAgentRunReviews",
|
||||
)
|
||||
async def list_reviews(
|
||||
run_id: Optional[str] = Query(default=None, description="Filter by run ID"),
|
||||
run_id: Optional[str] = Query(
|
||||
default=None, description="Filter by graph execution ID"
|
||||
),
|
||||
status: Optional[ReviewStatus] = Query(
|
||||
description="Filter by review status",
|
||||
),
|
||||
@@ -269,7 +278,7 @@ async def list_reviews(
|
||||
"""
|
||||
List human-in-the-loop reviews for agent runs.
|
||||
|
||||
Defaults to reviews with status WAITING if no status filter is given.
|
||||
Returns reviews with status WAITING if no status filter is given.
|
||||
"""
|
||||
reviews, pagination = await review_db.get_reviews(
|
||||
user_id=auth.user_id,
|
||||
@@ -290,11 +299,12 @@ async def list_reviews(
|
||||
|
||||
@runs_router.post(
|
||||
path="/{run_id}/reviews",
|
||||
summary="Submit run reviews",
|
||||
summary="Submit agent run human-in-the-loop reviews",
|
||||
operation_id="submitAgentRunReviews",
|
||||
)
|
||||
async def submit_reviews(
|
||||
request: AgentRunReviewsSubmitRequest,
|
||||
run_id: str,
|
||||
run_id: str = Path(description="Graph Execution ID"),
|
||||
auth: APIAuthorizationInfo = Security(
|
||||
require_permission(APIKeyPermission.WRITE_RUN_REVIEW)
|
||||
),
|
||||
|
||||
@@ -27,7 +27,7 @@ from .models import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
schedules_router = APIRouter()
|
||||
schedules_router = APIRouter(tags=["graphs", "schedules"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -37,7 +37,8 @@ schedules_router = APIRouter()
|
||||
|
||||
@schedules_router.get(
|
||||
path="",
|
||||
summary="List schedules",
|
||||
summary="List run schedules",
|
||||
operation_id="listGraphRunSchedules",
|
||||
)
|
||||
async def list_all_schedules(
|
||||
graph_id: Optional[str] = Query(default=None, description="Filter by graph ID"),
|
||||
@@ -77,7 +78,8 @@ async def list_all_schedules(
|
||||
|
||||
@schedules_router.delete(
|
||||
path="/{schedule_id}",
|
||||
summary="Delete schedule",
|
||||
summary="Delete run schedule",
|
||||
operation_id="deleteGraphRunSchedule",
|
||||
)
|
||||
async def delete_schedule(
|
||||
schedule_id: str,
|
||||
@@ -105,12 +107,13 @@ async def delete_schedule(
|
||||
# These are included in the graphs router via include_router
|
||||
# ============================================================================
|
||||
|
||||
graph_schedules_router = APIRouter()
|
||||
graph_schedules_router = APIRouter(tags=["graphs"])
|
||||
|
||||
|
||||
@graph_schedules_router.post(
|
||||
path="/{graph_id}/schedules",
|
||||
summary="Create graph schedule",
|
||||
summary="Create run schedule",
|
||||
operation_id="createGraphRunSchedule",
|
||||
)
|
||||
async def create_graph_schedule(
|
||||
request: AgentRunScheduleCreateRequest,
|
||||
|
||||
76
autogpt_platform/backend/backend/api/external/v2/search.py
vendored
Normal file
76
autogpt_platform/backend/backend/api/external/v2/search.py
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
V2 External API - Search Endpoints
|
||||
|
||||
Cross-domain hybrid search across agents, blocks, and documentation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Query, Security
|
||||
from prisma.enums import ContentType as SearchContentType
|
||||
|
||||
from backend.api.external.middleware import require_auth
|
||||
from backend.api.features.store.hybrid_search import unified_hybrid_search
|
||||
from backend.data.auth.base import APIAuthorizationInfo
|
||||
|
||||
from .common import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
|
||||
from .models import MarketplaceSearchResponse, MarketplaceSearchResult
|
||||
from .rate_limit import search_limiter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
search_router = APIRouter(tags=["search"])
|
||||
|
||||
|
||||
@search_router.get(
|
||||
path="",
|
||||
summary="Search content and capabilities of the platform",
|
||||
operation_id="search",
|
||||
)
|
||||
async def search(
|
||||
query: str = Query(description="Search query"),
|
||||
content_types: Optional[list[SearchContentType]] = Query(
|
||||
default=None, description="Content types to filter by"
|
||||
),
|
||||
category: Optional[str] = Query(default=None, description="Filter by category"),
|
||||
page: int = Query(ge=1, default=1),
|
||||
page_size: int = Query(ge=1, le=MAX_PAGE_SIZE, default=DEFAULT_PAGE_SIZE),
|
||||
auth: APIAuthorizationInfo = Security(require_auth),
|
||||
) -> MarketplaceSearchResponse:
|
||||
"""
|
||||
Search the platform's content and capabilities (hybrid search: literal + semantic).
|
||||
|
||||
Searches across agents, blocks, and documentation. Results are ranked
|
||||
by a combination of keyword matching and semantic similarity.
|
||||
"""
|
||||
search_limiter.check(auth.user_id)
|
||||
|
||||
results, total_count = await unified_hybrid_search(
|
||||
query=query,
|
||||
content_types=content_types,
|
||||
category=category,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
user_id=auth.user_id,
|
||||
)
|
||||
|
||||
total_pages = max(1, (total_count + page_size - 1) // page_size)
|
||||
|
||||
return MarketplaceSearchResponse(
|
||||
results=[
|
||||
MarketplaceSearchResult(
|
||||
content_type=r.get("content_type", ""),
|
||||
content_id=r.get("content_id", ""),
|
||||
searchable_text=r.get("searchable_text", ""),
|
||||
metadata=r.get("metadata"),
|
||||
updated_at=r.get("updated_at"),
|
||||
combined_score=r.get("combined_score"),
|
||||
)
|
||||
for r in results
|
||||
],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_count=total_count,
|
||||
total_pages=total_pages,
|
||||
)
|
||||
@@ -518,22 +518,22 @@ async def get_store_submissions(
|
||||
|
||||
async def delete_store_submission(
|
||||
user_id: str,
|
||||
submission_id: str,
|
||||
store_listing_version_id: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Delete a store submission version as the submitting user.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user
|
||||
submission_id: StoreListingVersion ID to delete
|
||||
store_listing_version_id: StoreListingVersion ID to delete
|
||||
|
||||
Returns:
|
||||
bool: True if successfully deleted
|
||||
"""
|
||||
try:
|
||||
# Find the submission version with ownership check
|
||||
version = await prisma.models.StoreListingVersion.prisma().find_first(
|
||||
where={"id": submission_id}, include={"StoreListing": True}
|
||||
version = await prisma.models.StoreListingVersion.prisma().find_unique(
|
||||
where={"id": store_listing_version_id}, include={"StoreListing": True}
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -546,7 +546,7 @@ async def delete_store_submission(
|
||||
# Prevent deletion of approved submissions
|
||||
if version.submissionStatus == prisma.enums.SubmissionStatus.APPROVED:
|
||||
raise store_exceptions.InvalidOperationError(
|
||||
"Cannot delete approved submissions"
|
||||
"Cannot delete approved store listings"
|
||||
)
|
||||
|
||||
# Delete the version
|
||||
|
||||
@@ -354,7 +354,7 @@ async def delete_submission(
|
||||
"""Delete a marketplace listing submission"""
|
||||
result = await store_db.delete_store_submission(
|
||||
user_id=user_id,
|
||||
submission_id=submission_id,
|
||||
store_listing_version_id=submission_id,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user