mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(platform): add external api routes for store search and tool usage (#11463)
We want to allow external tools to explore the marketplace and use the chat agent tools ### Changes 🏗️ - add store api routes - add tool api routes ### 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: <!-- Put your test plan here: --> - [x] tested all endpoints work --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,7 @@ from fastapi import FastAPI
|
||||
from backend.monitoring.instrumentation import instrument_fastapi
|
||||
from backend.server.middleware.security import SecurityHeadersMiddleware
|
||||
|
||||
from .routes.tools import tools_router
|
||||
from .routes.v1 import v1_router
|
||||
|
||||
external_app = FastAPI(
|
||||
@@ -14,6 +15,7 @@ external_app = FastAPI(
|
||||
|
||||
external_app.add_middleware(SecurityHeadersMiddleware)
|
||||
external_app.include_router(v1_router, prefix="/v1")
|
||||
external_app.include_router(tools_router, prefix="/v1")
|
||||
|
||||
# Add Prometheus instrumentation
|
||||
instrument_fastapi(
|
||||
|
||||
239
autogpt_platform/backend/backend/server/external/routes/tools.py
vendored
Normal file
239
autogpt_platform/backend/backend/server/external/routes/tools.py
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
"""External API routes for chat tools - stateless HTTP endpoints.
|
||||
|
||||
Note: These endpoints use ephemeral sessions that are not persisted to Redis.
|
||||
As a result, session-based rate limiting (max_agent_runs, max_agent_schedules)
|
||||
is not enforced for external API calls. Each request creates a fresh session
|
||||
with zeroed counters. Rate limiting for external API consumers should be
|
||||
handled separately (e.g., via API key quotas).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Security
|
||||
from prisma.enums import APIKeyPermission
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.data.api_key import APIKeyInfo
|
||||
from backend.server.external.middleware import require_permission
|
||||
from backend.server.v2.chat.model import ChatSession
|
||||
from backend.server.v2.chat.tools import (
|
||||
find_agent_tool,
|
||||
get_agent_details_tool,
|
||||
get_required_setup_info_tool,
|
||||
run_agent_tool,
|
||||
setup_agent_tool,
|
||||
)
|
||||
from backend.server.v2.chat.tools.models import ToolResponseBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tools_router = APIRouter(prefix="/tools", tags=["tools"])
|
||||
|
||||
# Note: We use Security() as a function parameter dependency (api_key: APIKeyInfo = Security(...))
|
||||
# rather than in the decorator's dependencies= list. This avoids duplicate permission checks
|
||||
# while still enforcing auth AND giving us access to the api_key for extracting user_id.
|
||||
|
||||
|
||||
# Request models
|
||||
class FindAgentRequest(BaseModel):
|
||||
query: str = Field(..., description="Search query for finding agents")
|
||||
|
||||
|
||||
class AgentSlugRequest(BaseModel):
|
||||
username_agent_slug: str = Field(
|
||||
...,
|
||||
description="The marketplace agent slug (e.g., 'username/agent-name')",
|
||||
)
|
||||
|
||||
|
||||
class GetRequiredSetupInfoRequest(AgentSlugRequest):
|
||||
inputs: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="The input dictionary you plan to provide",
|
||||
)
|
||||
|
||||
|
||||
class RunAgentRequest(AgentSlugRequest):
|
||||
inputs: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Dictionary of input values for the agent",
|
||||
)
|
||||
|
||||
|
||||
class SetupAgentRequest(AgentSlugRequest):
|
||||
setup_type: str = Field(
|
||||
default="schedule",
|
||||
description="Type of setup: 'schedule' for cron, 'webhook' for triggers",
|
||||
)
|
||||
name: str = Field(..., description="Name for this setup/schedule")
|
||||
description: str | None = Field(None, description="Description of this setup")
|
||||
cron: str | None = Field(
|
||||
None,
|
||||
description="Cron expression (5 fields: minute hour day month weekday)",
|
||||
)
|
||||
timezone: str = Field(
|
||||
default="UTC",
|
||||
description="IANA timezone (e.g., 'America/New_York', 'UTC')",
|
||||
)
|
||||
inputs: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Dictionary with required inputs for the agent",
|
||||
)
|
||||
webhook_config: dict[str, Any] | None = Field(
|
||||
None,
|
||||
description="Webhook configuration (required if setup_type is 'webhook')",
|
||||
)
|
||||
|
||||
|
||||
def _create_ephemeral_session(user_id: str | None) -> ChatSession:
|
||||
"""Create an ephemeral session for stateless API requests.
|
||||
|
||||
Note: These sessions are NOT persisted to Redis, so session-based rate
|
||||
limiting (max_agent_runs, max_agent_schedules) will not be enforced
|
||||
across requests.
|
||||
"""
|
||||
return ChatSession.new(user_id)
|
||||
|
||||
|
||||
@tools_router.post(path="/find-agent")
|
||||
async def find_agent(
|
||||
request: FindAgentRequest,
|
||||
api_key: APIKeyInfo = Security(require_permission(APIKeyPermission.USE_TOOLS)),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Search for agents in the marketplace based on capabilities and user needs.
|
||||
|
||||
Args:
|
||||
request: Search query for finding agents
|
||||
|
||||
Returns:
|
||||
List of matching agents or no results response
|
||||
"""
|
||||
session = _create_ephemeral_session(api_key.user_id)
|
||||
result = await find_agent_tool._execute(
|
||||
user_id=api_key.user_id,
|
||||
session=session,
|
||||
query=request.query,
|
||||
)
|
||||
return _response_to_dict(result)
|
||||
|
||||
|
||||
@tools_router.post(path="/get-agent-details")
|
||||
async def get_agent_details(
|
||||
request: AgentSlugRequest,
|
||||
api_key: APIKeyInfo = Security(require_permission(APIKeyPermission.USE_TOOLS)),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get detailed information about a specific agent including inputs,
|
||||
credentials required, and execution options.
|
||||
|
||||
Args:
|
||||
request: Agent slug in format 'username/agent-name'
|
||||
|
||||
Returns:
|
||||
Detailed agent information
|
||||
"""
|
||||
session = _create_ephemeral_session(api_key.user_id)
|
||||
result = await get_agent_details_tool._execute(
|
||||
user_id=api_key.user_id,
|
||||
session=session,
|
||||
username_agent_slug=request.username_agent_slug,
|
||||
)
|
||||
return _response_to_dict(result)
|
||||
|
||||
|
||||
@tools_router.post(path="/get-required-setup-info")
|
||||
async def get_required_setup_info(
|
||||
request: GetRequiredSetupInfoRequest,
|
||||
api_key: APIKeyInfo = Security(require_permission(APIKeyPermission.USE_TOOLS)),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Check if an agent can be set up with the provided input data and credentials.
|
||||
Validates that you have all required inputs before running or scheduling.
|
||||
|
||||
Args:
|
||||
request: Agent slug and optional inputs to validate
|
||||
|
||||
Returns:
|
||||
Setup requirements and user readiness status
|
||||
"""
|
||||
session = _create_ephemeral_session(api_key.user_id)
|
||||
result = await get_required_setup_info_tool._execute(
|
||||
user_id=api_key.user_id,
|
||||
session=session,
|
||||
username_agent_slug=request.username_agent_slug,
|
||||
inputs=request.inputs,
|
||||
)
|
||||
return _response_to_dict(result)
|
||||
|
||||
|
||||
@tools_router.post(path="/run-agent")
|
||||
async def run_agent(
|
||||
request: RunAgentRequest,
|
||||
api_key: APIKeyInfo = Security(require_permission(APIKeyPermission.USE_TOOLS)),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Run an agent immediately (one-off manual execution).
|
||||
|
||||
IMPORTANT: Before calling this endpoint, first call get-agent-details
|
||||
to determine what inputs are required.
|
||||
|
||||
Args:
|
||||
request: Agent slug and input values
|
||||
|
||||
Returns:
|
||||
Execution started response with execution_id
|
||||
"""
|
||||
session = _create_ephemeral_session(api_key.user_id)
|
||||
result = await run_agent_tool._execute(
|
||||
user_id=api_key.user_id,
|
||||
session=session,
|
||||
username_agent_slug=request.username_agent_slug,
|
||||
inputs=request.inputs,
|
||||
)
|
||||
return _response_to_dict(result)
|
||||
|
||||
|
||||
@tools_router.post(path="/setup-agent")
|
||||
async def setup_agent(
|
||||
request: SetupAgentRequest,
|
||||
api_key: APIKeyInfo = Security(require_permission(APIKeyPermission.USE_TOOLS)),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Set up an agent with credentials and configure it for scheduled execution
|
||||
or webhook triggers.
|
||||
|
||||
For SCHEDULED execution:
|
||||
- Cron format: "minute hour day month weekday"
|
||||
- Examples: "0 9 * * 1-5" (9am weekdays), "0 0 * * *" (daily at midnight)
|
||||
- Timezone: Use IANA timezone names like "America/New_York", "Europe/London"
|
||||
|
||||
For WEBHOOK triggers:
|
||||
- The agent will be triggered by external events
|
||||
|
||||
Args:
|
||||
request: Agent slug, setup type, schedule configuration, and inputs
|
||||
|
||||
Returns:
|
||||
Schedule or webhook created response
|
||||
"""
|
||||
session = _create_ephemeral_session(api_key.user_id)
|
||||
result = await setup_agent_tool._execute(
|
||||
user_id=api_key.user_id,
|
||||
session=session,
|
||||
username_agent_slug=request.username_agent_slug,
|
||||
setup_type=request.setup_type,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
cron=request.cron,
|
||||
timezone=request.timezone,
|
||||
inputs=request.inputs,
|
||||
webhook_config=request.webhook_config,
|
||||
)
|
||||
return _response_to_dict(result)
|
||||
|
||||
|
||||
def _response_to_dict(result: ToolResponseBase) -> dict[str, Any]:
|
||||
"""Convert a tool response to a dictionary for JSON serialization."""
|
||||
return result.model_dump()
|
||||
@@ -1,12 +1,15 @@
|
||||
import logging
|
||||
import urllib.parse
|
||||
from collections import defaultdict
|
||||
from typing import Annotated, Any, Optional, Sequence
|
||||
from typing import Annotated, Any, Literal, Optional, Sequence
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Security
|
||||
from prisma.enums import AgentExecutionStatus, APIKeyPermission
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
import backend.data.block
|
||||
import backend.server.v2.store.cache as store_cache
|
||||
import backend.server.v2.store.model as store_model
|
||||
from backend.data import execution as execution_db
|
||||
from backend.data import graph as graph_db
|
||||
from backend.data.api_key import APIKeyInfo
|
||||
@@ -144,3 +147,149 @@ async def get_graph_execution_results(
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
##############################################
|
||||
############### Store Endpoints ##############
|
||||
##############################################
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
path="/store/agents",
|
||||
tags=["store"],
|
||||
dependencies=[Security(require_permission(APIKeyPermission.READ_STORE))],
|
||||
response_model=store_model.StoreAgentsResponse,
|
||||
)
|
||||
async def get_store_agents(
|
||||
featured: bool = False,
|
||||
creator: str | None = None,
|
||||
sorted_by: Literal["rating", "runs", "name", "updated_at"] | None = None,
|
||||
search_query: str | None = None,
|
||||
category: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> store_model.StoreAgentsResponse:
|
||||
"""
|
||||
Get a paginated list of agents from the store with optional filtering and sorting.
|
||||
|
||||
Args:
|
||||
featured: Filter to only show featured agents
|
||||
creator: Filter agents by creator username
|
||||
sorted_by: Sort agents by "runs", "rating", "name", or "updated_at"
|
||||
search_query: Search agents by name, subheading and description
|
||||
category: Filter agents by category
|
||||
page: Page number for pagination (default 1)
|
||||
page_size: Number of agents per page (default 20)
|
||||
|
||||
Returns:
|
||||
StoreAgentsResponse: Paginated list of agents matching the filters
|
||||
"""
|
||||
if page < 1:
|
||||
raise HTTPException(status_code=422, detail="Page must be greater than 0")
|
||||
|
||||
if page_size < 1:
|
||||
raise HTTPException(status_code=422, detail="Page size must be greater than 0")
|
||||
|
||||
agents = await store_cache._get_cached_store_agents(
|
||||
featured=featured,
|
||||
creator=creator,
|
||||
sorted_by=sorted_by,
|
||||
search_query=search_query,
|
||||
category=category,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return agents
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
path="/store/agents/{username}/{agent_name}",
|
||||
tags=["store"],
|
||||
dependencies=[Security(require_permission(APIKeyPermission.READ_STORE))],
|
||||
response_model=store_model.StoreAgentDetails,
|
||||
)
|
||||
async def get_store_agent(
|
||||
username: str,
|
||||
agent_name: str,
|
||||
) -> store_model.StoreAgentDetails:
|
||||
"""
|
||||
Get details of a specific store agent by username and agent name.
|
||||
|
||||
Args:
|
||||
username: Creator's username
|
||||
agent_name: Name/slug of the agent
|
||||
|
||||
Returns:
|
||||
StoreAgentDetails: Detailed information about the agent
|
||||
"""
|
||||
username = urllib.parse.unquote(username).lower()
|
||||
agent_name = urllib.parse.unquote(agent_name).lower()
|
||||
agent = await store_cache._get_cached_agent_details(
|
||||
username=username, agent_name=agent_name
|
||||
)
|
||||
return agent
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
path="/store/creators",
|
||||
tags=["store"],
|
||||
dependencies=[Security(require_permission(APIKeyPermission.READ_STORE))],
|
||||
response_model=store_model.CreatorsResponse,
|
||||
)
|
||||
async def get_store_creators(
|
||||
featured: bool = False,
|
||||
search_query: str | None = None,
|
||||
sorted_by: Literal["agent_rating", "agent_runs", "num_agents"] | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> store_model.CreatorsResponse:
|
||||
"""
|
||||
Get a paginated list of store creators with optional filtering and sorting.
|
||||
|
||||
Args:
|
||||
featured: Filter to only show featured creators
|
||||
search_query: Search creators by profile description
|
||||
sorted_by: Sort by "agent_rating", "agent_runs", or "num_agents"
|
||||
page: Page number for pagination (default 1)
|
||||
page_size: Number of creators per page (default 20)
|
||||
|
||||
Returns:
|
||||
CreatorsResponse: Paginated list of creators matching the filters
|
||||
"""
|
||||
if page < 1:
|
||||
raise HTTPException(status_code=422, detail="Page must be greater than 0")
|
||||
|
||||
if page_size < 1:
|
||||
raise HTTPException(status_code=422, detail="Page size must be greater than 0")
|
||||
|
||||
creators = await store_cache._get_cached_store_creators(
|
||||
featured=featured,
|
||||
search_query=search_query,
|
||||
sorted_by=sorted_by,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return creators
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
path="/store/creators/{username}",
|
||||
tags=["store"],
|
||||
dependencies=[Security(require_permission(APIKeyPermission.READ_STORE))],
|
||||
response_model=store_model.CreatorDetails,
|
||||
)
|
||||
async def get_store_creator(
|
||||
username: str,
|
||||
) -> store_model.CreatorDetails:
|
||||
"""
|
||||
Get details of a specific store creator by username.
|
||||
|
||||
Args:
|
||||
username: Creator's username
|
||||
|
||||
Returns:
|
||||
CreatorDetails: Detailed information about the creator
|
||||
"""
|
||||
username = urllib.parse.unquote(username).lower()
|
||||
creator = await store_cache._get_cached_creator_details(username=username)
|
||||
return creator
|
||||
|
||||
@@ -64,7 +64,10 @@ async def create_session(
|
||||
CreateSessionResponse: Details of the created session.
|
||||
|
||||
"""
|
||||
logger.info(f"Creating session with user_id: {user_id}")
|
||||
logger.info(
|
||||
f"Creating session with user_id: "
|
||||
f"...{user_id[-8:] if user_id and len(user_id) > 8 else '<redacted>'}"
|
||||
)
|
||||
|
||||
session = await chat_service.create_chat_session(user_id)
|
||||
|
||||
|
||||
@@ -396,8 +396,7 @@ async def create_library_agent(
|
||||
DatabaseError: If there's an error during creation or if image generation fails.
|
||||
"""
|
||||
logger.info(
|
||||
f"Creating library agent for graph #{graph.id} v{graph.version}; "
|
||||
f"user #{user_id}"
|
||||
f"Creating library agent for graph #{graph.id} v{graph.version}; user:<redacted>"
|
||||
)
|
||||
graph_entries = (
|
||||
[graph, *graph.sub_graphs] if create_library_agents_for_sub_graphs else [graph]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "APIKeyPermission" ADD VALUE 'READ_STORE';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "APIKeyPermission" ADD VALUE 'USE_TOOLS';
|
||||
@@ -910,6 +910,8 @@ enum APIKeyPermission {
|
||||
READ_GRAPH // Can get graph versions and details
|
||||
EXECUTE_BLOCK // Can execute individual blocks
|
||||
READ_BLOCK // Can get block information
|
||||
READ_STORE // Can read store agents and creators
|
||||
USE_TOOLS // Can use chat tools via external API
|
||||
}
|
||||
|
||||
model APIKey {
|
||||
|
||||
@@ -5291,7 +5291,14 @@
|
||||
},
|
||||
"APIKeyPermission": {
|
||||
"type": "string",
|
||||
"enum": ["EXECUTE_GRAPH", "READ_GRAPH", "EXECUTE_BLOCK", "READ_BLOCK"],
|
||||
"enum": [
|
||||
"EXECUTE_GRAPH",
|
||||
"READ_GRAPH",
|
||||
"EXECUTE_BLOCK",
|
||||
"READ_BLOCK",
|
||||
"READ_STORE",
|
||||
"USE_TOOLS"
|
||||
],
|
||||
"title": "APIKeyPermission"
|
||||
},
|
||||
"APIKeyStatus": {
|
||||
|
||||
Reference in New Issue
Block a user