mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(market): Agent Submission Process (#7718)
* adding auth to store * Add ability to submit agents and review them before being added to the market * Added auth decorator * Added auth to market api client * fix(builder): Fix drag-select behavior on `NodeKeyValueInput` * fix(github): Added in fallback variables for postgres testing (#7715) Co-authored-by: Leslie Cruz <lelcruz@users.noreply.github.com> Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co> * feat(builder): basic tally feedback form (#7725) * removed database changes * moved auth to libs project * fixed formatting * cleaned up auth * Added tests and database migration * delete decorator
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import contextlib
|
||||
import logging.config
|
||||
import os
|
||||
|
||||
import dotenv
|
||||
@@ -12,12 +13,15 @@ import sentry_sdk.integrations.asyncio
|
||||
import sentry_sdk.integrations.fastapi
|
||||
import sentry_sdk.integrations.starlette
|
||||
|
||||
import market.config
|
||||
import market.routes.admin
|
||||
import market.routes.agents
|
||||
import market.routes.search
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
logging.config.dictConfig(market.config.LogConfig().model_dump())
|
||||
|
||||
if os.environ.get("SENTRY_DSN"):
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN"),
|
||||
|
||||
30
rnd/market/market/config.py
Normal file
30
rnd/market/market/config.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class LogConfig(BaseModel):
|
||||
"""Logging configuration to be set for the server"""
|
||||
|
||||
LOGGER_NAME: str = "marketplace"
|
||||
LOG_FORMAT: str = "%(levelprefix)s | %(asctime)s | %(message)s"
|
||||
LOG_LEVEL: str = "DEBUG"
|
||||
|
||||
# Logging config
|
||||
version: int = 1
|
||||
disable_existing_loggers: bool = False
|
||||
formatters: dict = {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": LOG_FORMAT,
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
}
|
||||
handlers: dict = {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
}
|
||||
loggers: dict = {
|
||||
LOGGER_NAME: {"handlers": ["default"], "level": LOG_LEVEL},
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import fuzzywuzzy.fuzz
|
||||
import prisma.enums
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
import prisma.types
|
||||
@@ -61,6 +63,7 @@ async def create_agent_entry(
|
||||
keywords: typing.List[str],
|
||||
categories: typing.List[str],
|
||||
graph: prisma.Json,
|
||||
submission_state: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.PENDING,
|
||||
):
|
||||
"""
|
||||
Create a new agent entry in the database.
|
||||
@@ -89,6 +92,7 @@ async def create_agent_entry(
|
||||
"categories": categories,
|
||||
"graph": graph,
|
||||
"AnalyticsTracker": {"create": {"downloads": 0, "views": 0}},
|
||||
"submissionStatus": submission_state,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -100,6 +104,39 @@ async def create_agent_entry(
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def update_agent_entry(
|
||||
agent_id: str,
|
||||
version: int,
|
||||
submission_state: prisma.enums.SubmissionStatus,
|
||||
comments: str | None = None,
|
||||
):
|
||||
"""
|
||||
Update an existing agent entry in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int): The version of the agent.
|
||||
submission_state (prisma.enums.SubmissionStatus): The submission state of the agent.
|
||||
"""
|
||||
|
||||
try:
|
||||
agent = await prisma.models.Agents.prisma().update(
|
||||
where={"id": agent_id},
|
||||
data={
|
||||
"version": version,
|
||||
"submissionStatus": submission_state,
|
||||
"submissionReviewDate": datetime.datetime.now(datetime.timezone.utc),
|
||||
"submissionReviewComments": comments,
|
||||
},
|
||||
)
|
||||
|
||||
return agent
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Agent Update Failed Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_agents(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
@@ -108,6 +145,7 @@ async def get_agents(
|
||||
category: str | None = None,
|
||||
description: str | None = None,
|
||||
description_threshold: int = 60,
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
sort_by: str = "createdAt",
|
||||
sort_order: typing.Literal["desc"] | typing.Literal["asc"] = "desc",
|
||||
):
|
||||
@@ -140,6 +178,8 @@ async def get_agents(
|
||||
if category:
|
||||
query["categories"] = {"has": category}
|
||||
|
||||
query["submissionStatus"] = submission_status
|
||||
|
||||
# Define sorting
|
||||
order = {sort_by: sort_order}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import prisma.enums
|
||||
import pydantic
|
||||
|
||||
|
||||
@@ -11,6 +12,13 @@ class AddAgentRequest(pydantic.BaseModel):
|
||||
categories: list[str]
|
||||
|
||||
|
||||
class SubmissionReviewRequest(pydantic.BaseModel):
|
||||
agent_id: str
|
||||
version: int
|
||||
status: prisma.enums.SubmissionStatus
|
||||
comments: str | None
|
||||
|
||||
|
||||
class AgentResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents a response from an agent.
|
||||
@@ -36,6 +44,7 @@ class AgentResponse(pydantic.BaseModel):
|
||||
version: int
|
||||
createdAt: datetime.datetime
|
||||
updatedAt: datetime.datetime
|
||||
submission_status: str
|
||||
views: int = 0
|
||||
downloads: int = 0
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
import autogpt_libs.auth
|
||||
import fastapi
|
||||
import prisma
|
||||
import prisma.enums
|
||||
import prisma.models
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
|
||||
logger = logging.getLogger("marketplace")
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@@ -79,3 +85,96 @@ async def unset_agent_featured(
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/agent/submissions", response_model=market.model.AgentListResponse)
|
||||
async def get_agent_submissions(
|
||||
page: int = fastapi.Query(1, ge=1, description="Page number"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, ge=1, le=100, description="Number of items per page"
|
||||
),
|
||||
name: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by agent name"
|
||||
),
|
||||
keyword: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by keyword"
|
||||
),
|
||||
category: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Filter by category"
|
||||
),
|
||||
description: typing.Optional[str] = fastapi.Query(
|
||||
None, description="Fuzzy search in description"
|
||||
),
|
||||
description_threshold: int = fastapi.Query(
|
||||
60, ge=0, le=100, description="Fuzzy search threshold"
|
||||
),
|
||||
sort_by: str = fastapi.Query("createdAt", description="Field to sort by"),
|
||||
sort_order: typing.Literal["asc", "desc"] = fastapi.Query(
|
||||
"desc", description="Sort order (asc or desc)"
|
||||
),
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
logger.info("Getting agent submissions")
|
||||
try:
|
||||
result = await market.db.get_agents(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
name=name,
|
||||
keyword=keyword,
|
||||
category=category,
|
||||
description=description,
|
||||
description_threshold=description_threshold,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
submission_status=prisma.enums.SubmissionStatus.PENDING,
|
||||
)
|
||||
|
||||
agents = [
|
||||
market.model.AgentResponse(**agent.dict()) for agent in result["agents"]
|
||||
]
|
||||
|
||||
return market.model.AgentListResponse(
|
||||
agents=agents,
|
||||
total_count=result["total_count"],
|
||||
page=result["page"],
|
||||
page_size=result["page_size"],
|
||||
total_pages=result["total_pages"],
|
||||
)
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
logger.error(f"Error getting agent submissions: {e}")
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting agent submissions: {e}")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/agent/submissions")
|
||||
async def review_submission(
|
||||
review_request: market.model.SubmissionReviewRequest,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
"""
|
||||
A basic endpoint to review a submission in the database.
|
||||
"""
|
||||
logger.info(
|
||||
f"Reviewing submission: {review_request.agent_id}, {review_request.version}"
|
||||
)
|
||||
try:
|
||||
# await market.db.update_agent_entry(
|
||||
# agent_id=review_request.agent_id,
|
||||
# version=review_request.version,
|
||||
# submission_state=review_request.status,
|
||||
# comments=review_request.comments,
|
||||
# )
|
||||
return fastapi.responses.Response(status_code=200)
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
76
rnd/market/market/routes/admin_tests.py
Normal file
76
rnd/market/market/routes/admin_tests.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import fastapi.testclient
|
||||
import prisma.enums
|
||||
import prisma.models
|
||||
|
||||
import market.app
|
||||
|
||||
client = fastapi.testclient.TestClient(market.app.app)
|
||||
|
||||
|
||||
async def override_auth_middleware(request: fastapi.Request):
|
||||
return {"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"}
|
||||
|
||||
|
||||
market.app.app.dependency_overrides[autogpt_libs.auth.middleware.auth_middleware] = (
|
||||
override_auth_middleware
|
||||
)
|
||||
|
||||
|
||||
def test_get_submissions():
|
||||
with mock.patch("market.db.get_agents") as mock_get_agents:
|
||||
mock_get_agents.return_value = {
|
||||
"agents": [],
|
||||
"total_count": 0,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 0,
|
||||
}
|
||||
response = client.get(
|
||||
"/api/v1/market/admin/agent/submissions?page=1&page_size=10&description_threshold=60&sort_by=createdAt&sort_order=desc",
|
||||
headers={"Bearer": ""},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"agents": [],
|
||||
"total_count": 0,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_review_submission():
|
||||
with mock.patch("market.db.update_agent_entry") as mock_update_agent_entry:
|
||||
mock_update_agent_entry.return_value = prisma.models.Agents(
|
||||
id="aaa-bbb-ccc",
|
||||
version=1,
|
||||
createdAt=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
updatedAt=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
submissionStatus=prisma.enums.SubmissionStatus.APPROVED,
|
||||
submissionDate=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
|
||||
submissionReviewComments="Looks good",
|
||||
submissionReviewDate=datetime.datetime.fromisoformat(
|
||||
"2021-10-01T00:00:00+00:00"
|
||||
),
|
||||
keywords=["test"],
|
||||
categories=["test"],
|
||||
graph='{"name": "test", "description": "test"}', # type: ignore
|
||||
)
|
||||
response = client.post(
|
||||
"/api/v1/market/admin/agent/submissions",
|
||||
headers={
|
||||
"Authorization": "Bearer token"
|
||||
}, # Assuming you need an authorization token
|
||||
json={
|
||||
"agent_id": "aaa-bbb-ccc",
|
||||
"version": 1,
|
||||
"status": "APPROVED",
|
||||
"comments": "Looks good",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -248,6 +248,7 @@ async def top_agents_by_downloads(
|
||||
updatedAt=item.agent.updatedAt,
|
||||
views=item.views,
|
||||
downloads=item.downloads,
|
||||
submission_status=item.agent.submissionStatus,
|
||||
)
|
||||
for item in result.analytics
|
||||
if item.agent is not None
|
||||
@@ -323,6 +324,7 @@ async def get_featured_agents(
|
||||
and len(item.agent.AnalyticsTracker) > 0
|
||||
else 0
|
||||
),
|
||||
submission_status=item.agent.submissionStatus,
|
||||
)
|
||||
for item in result.featured_agents
|
||||
if item.agent is not None
|
||||
|
||||
35
rnd/market/market/routes/submissions.py
Normal file
35
rnd/market/market/routes/submissions.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import autogpt_libs.auth
|
||||
import fastapi
|
||||
import fastapi.responses
|
||||
import prisma
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
import market.utils.analytics
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.post("/agents/submit", response_model=market.model.AgentResponse)
|
||||
async def submit_agent(
|
||||
request: market.model.AddAgentRequest,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(autogpt_libs.auth.requires_user),
|
||||
):
|
||||
"""
|
||||
A basic endpoint to create a new agent entry in the database.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.create_agent_entry(
|
||||
request.graph["name"],
|
||||
request.graph["description"],
|
||||
request.author,
|
||||
request.keywords,
|
||||
request.categories,
|
||||
prisma.Json(request.graph),
|
||||
)
|
||||
|
||||
return fastapi.responses.PlainTextResponse(agent.model_dump_json())
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1,8 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubmissionStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Agents" ADD COLUMN "submissionDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "submissionReviewComments" TEXT,
|
||||
ADD COLUMN "submissionReviewDate" TIMESTAMP(3),
|
||||
ADD COLUMN "submissionStatus" "SubmissionStatus" NOT NULL DEFAULT 'PENDING';
|
||||
@@ -11,12 +11,23 @@ generator client {
|
||||
partial_type_generator = "market/utils/partial_types.py"
|
||||
}
|
||||
|
||||
enum SubmissionStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
model Agents {
|
||||
id String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
id String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
version Int @default(1)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
version Int @default(1)
|
||||
submissionDate DateTime @default(now())
|
||||
submissionReviewDate DateTime?
|
||||
submissionStatus SubmissionStatus @default(PENDING)
|
||||
submissionReviewComments String?
|
||||
|
||||
name String?
|
||||
description String?
|
||||
|
||||
Reference in New Issue
Block a user