mirror of
https://github.com/Significant-Gravitas/Auto-GPT.git
synced 2025-01-08 11:57:32 +08:00
parent
5959c0d303
commit
da10a2a8ef
@ -144,51 +144,6 @@ services:
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
market:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: autogpt_platform/market/Dockerfile
|
||||
develop:
|
||||
watch:
|
||||
- path: ./
|
||||
target: autogpt_platform/market/
|
||||
action: rebuild
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
market-migrations:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
- SUPABASE_URL=http://kong:8000
|
||||
- SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
- SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
- DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=market
|
||||
- BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
|
||||
ports:
|
||||
- "8015:8015"
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
market-migrations:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: autogpt_platform/market/Dockerfile
|
||||
command: ["sh", "-c", "poetry run prisma migrate deploy"]
|
||||
develop:
|
||||
watch:
|
||||
- path: ./
|
||||
target: autogpt_platform/market/
|
||||
action: rebuild
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- SUPABASE_URL=http://kong:8000
|
||||
- SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
- SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
- DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=market
|
||||
networks:
|
||||
- app-network
|
||||
# frontend:
|
||||
# build:
|
||||
# context: ../
|
||||
|
@ -51,18 +51,6 @@ services:
|
||||
file: ./docker-compose.platform.yml
|
||||
service: websocket_server
|
||||
|
||||
market:
|
||||
<<: *agpt-services
|
||||
extends:
|
||||
file: ./docker-compose.platform.yml
|
||||
service: market
|
||||
|
||||
market-migrations:
|
||||
<<: *agpt-services
|
||||
extends:
|
||||
file: ./docker-compose.platform.yml
|
||||
service: market-migrations
|
||||
|
||||
# frontend:
|
||||
# <<: *agpt-services
|
||||
# extends:
|
||||
|
@ -1,12 +0,0 @@
|
||||
DB_USER=postgres
|
||||
DB_PASS=your-super-secret-and-long-postgres-password
|
||||
DB_NAME=postgres
|
||||
DB_PORT=5432
|
||||
DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}?connect_timeout=60&schema=market"
|
||||
SENTRY_DSN=https://11d0640fef35640e0eb9f022eb7d7626@o4505260022104064.ingest.us.sentry.io/4507890252447744
|
||||
|
||||
ENABLE_AUTH=true
|
||||
SUPABASE_JWT_SECRET=our-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
|
||||
|
||||
APP_ENV=local
|
6
autogpt_platform/market/.gitignore
vendored
6
autogpt_platform/market/.gitignore
vendored
@ -1,6 +0,0 @@
|
||||
database.db
|
||||
database.db-journal
|
||||
build/
|
||||
config.json
|
||||
secrets/*
|
||||
!secrets/.gitkeep
|
@ -1,72 +0,0 @@
|
||||
FROM python:3.11.10-slim-bookworm AS builder
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN echo 'Acquire::http::Pipeline-Depth 0;\nAcquire::http::No-Cache true;\nAcquire::BrokenProxy true;\n' > /etc/apt/apt.conf.d/99fixbadproxy
|
||||
|
||||
RUN apt-get update --allow-releaseinfo-change --fix-missing
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get install -y build-essential
|
||||
RUN apt-get install -y libpq5
|
||||
RUN apt-get install -y libz-dev
|
||||
RUN apt-get install -y libssl-dev
|
||||
|
||||
ENV POETRY_VERSION=1.8.3 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=false
|
||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||
|
||||
# Upgrade pip and setuptools to fix security vulnerabilities
|
||||
RUN pip3 install --upgrade pip setuptools
|
||||
|
||||
RUN pip3 install poetry
|
||||
|
||||
# Copy and install dependencies
|
||||
COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
|
||||
COPY autogpt_platform/market/poetry.lock autogpt_platform/market/pyproject.toml /app/autogpt_platform/market/
|
||||
WORKDIR /app/autogpt_platform/market
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry install --no-interaction --no-ansi
|
||||
|
||||
# Generate Prisma client
|
||||
COPY autogpt_platform/market /app/autogpt_platform/market
|
||||
RUN poetry config virtualenvs.create false \
|
||||
&& poetry run prisma generate
|
||||
|
||||
FROM python:3.11.10-slim-bookworm AS server_dependencies
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Upgrade pip and setuptools to fix security vulnerabilities
|
||||
RUN pip3 install --upgrade pip setuptools
|
||||
|
||||
# Copy only necessary files from builder
|
||||
COPY --from=builder /app /app
|
||||
COPY --from=builder /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
# Copy Prisma binaries
|
||||
COPY --from=builder /root/.cache/prisma-python/binaries /root/.cache/prisma-python/binaries
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
RUN mkdir -p /app/autogpt_platform/autogpt_libs
|
||||
RUN mkdir -p /app/autogpt_platform/market
|
||||
|
||||
COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
|
||||
|
||||
COPY autogpt_platform/market /app/autogpt_platform/market
|
||||
|
||||
WORKDIR /app/autogpt_platform/market
|
||||
|
||||
FROM server_dependencies AS server
|
||||
|
||||
ENV DATABASE_URL=""
|
||||
ENV PORT=8015
|
||||
|
||||
CMD ["poetry", "run", "app"]
|
@ -1,37 +0,0 @@
|
||||
# AutoGPT Agent Marketplace
|
||||
|
||||
## Overview
|
||||
|
||||
AutoGPT Agent Marketplace is an open-source platform for autonomous AI agents. This project aims to create a user-friendly, accessible marketplace where users can discover, utilize, and contribute to a diverse ecosystem of AI solutions.
|
||||
|
||||
## Vision
|
||||
|
||||
Our vision is to empower users with customizable and free AI agents, fostering an open-source community that drives innovation in AI automation across various industries.
|
||||
|
||||
## Key Features
|
||||
|
||||
- Agent Discovery and Search
|
||||
- Agent Listings with Detailed Information
|
||||
- User Profiles
|
||||
- Data Protection and Compliance
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with the AutoGPT Agent Marketplace, follow these steps:
|
||||
|
||||
- Copy `.env.example` to `.env` and fill in the required environment variables
|
||||
- Run `poetry run setup`
|
||||
- Run `poetry run populate`
|
||||
- Run `poetry run app`
|
||||
|
||||
## Poetry Run Commands
|
||||
|
||||
This section outlines the available command line scripts for this project, configured using Poetry. You can execute these scripts directly using Poetry. Each command performs a specific operation as described below:
|
||||
|
||||
- `poetry run format`: Runs the formatting script to ensure code consistency.
|
||||
- `poetry run lint`: Executes the linting script to identify and fix potential code issues.
|
||||
- `poetry run app`: Starts the main application.
|
||||
- `poetry run setup`: Runs the setup script to configure the database.
|
||||
- `poetry run populate`: Populates the database with initial data using the specified script.
|
||||
|
||||
To run any of these commands, ensure Poetry is installed on your system and execute the commands from the project's root directory.
|
@ -1,16 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
postgres:
|
||||
image: ankane/pgvector:latest
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_PASSWORD: ${DB_PASS}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
PGUSER: ${DB_USER}
|
||||
healthcheck:
|
||||
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- "${DB_PORT}:5432"
|
@ -1,97 +0,0 @@
|
||||
import contextlib
|
||||
import logging.config
|
||||
import os
|
||||
|
||||
import dotenv
|
||||
import fastapi
|
||||
import fastapi.middleware.cors
|
||||
import fastapi.middleware.gzip
|
||||
import prisma
|
||||
import prometheus_fastapi_instrumentator
|
||||
import sentry_sdk
|
||||
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.analytics
|
||||
import market.routes.search
|
||||
import market.routes.submissions
|
||||
|
||||
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"),
|
||||
traces_sample_rate=1.0,
|
||||
profiles_sample_rate=1.0,
|
||||
enable_tracing=True,
|
||||
environment=os.environ.get("RUN_ENV", default="CLOUD").lower(),
|
||||
integrations=[
|
||||
sentry_sdk.integrations.starlette.StarletteIntegration(
|
||||
transaction_style="url"
|
||||
),
|
||||
sentry_sdk.integrations.fastapi.FastApiIntegration(transaction_style="url"),
|
||||
sentry_sdk.integrations.asyncio.AsyncioIntegration(),
|
||||
],
|
||||
)
|
||||
|
||||
db_client = prisma.Prisma(auto_register=True)
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(app: fastapi.FastAPI):
|
||||
await db_client.connect()
|
||||
yield
|
||||
await db_client.disconnect()
|
||||
|
||||
|
||||
docs_url = "/docs"
|
||||
app = fastapi.FastAPI(
|
||||
title="Marketplace API",
|
||||
description="AutoGPT Marketplace API is a service that allows users to share AI agents.",
|
||||
summary="Maketplace API",
|
||||
version="0.1",
|
||||
lifespan=lifespan,
|
||||
root_path="/api/v1/market",
|
||||
docs_url=docs_url,
|
||||
)
|
||||
|
||||
app.add_middleware(fastapi.middleware.gzip.GZipMiddleware, minimum_size=1000)
|
||||
app.add_middleware(
|
||||
middleware_class=fastapi.middleware.cors.CORSMiddleware,
|
||||
allow_origins=os.environ.get(
|
||||
"BACKEND_CORS_ALLOW_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000"
|
||||
).split(","),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.include_router(market.routes.agents.router, tags=["agents"])
|
||||
app.include_router(market.routes.search.router, tags=["search"])
|
||||
app.include_router(market.routes.submissions.router, tags=["submissions"])
|
||||
app.include_router(market.routes.admin.router, prefix="/admin", tags=["admin"])
|
||||
app.include_router(
|
||||
market.routes.analytics.router, prefix="/analytics", tags=["analytics"]
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return fastapi.responses.HTMLResponse(
|
||||
content="<h1>Marketplace API</h1>", status_code=200
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def default():
|
||||
return fastapi.responses.HTMLResponse(
|
||||
content="<h1>Marketplace API</h1>", status_code=200
|
||||
)
|
||||
|
||||
|
||||
prometheus_fastapi_instrumentator.Instrumentator().instrument(app).expose(app)
|
@ -1,30 +0,0 @@
|
||||
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,725 +0,0 @@
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import fuzzywuzzy.fuzz
|
||||
import prisma.enums
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
import prisma.types
|
||||
import pydantic
|
||||
|
||||
import market.model
|
||||
import market.utils.extension_types
|
||||
|
||||
|
||||
class AgentQueryError(Exception):
|
||||
"""Custom exception for agent query errors"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TopAgentsDBResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents a response containing a list of top agents.
|
||||
|
||||
Attributes:
|
||||
analytics (list[AgentResponse]): The list of top agents.
|
||||
total_count (int): The total count of agents.
|
||||
page (int): The current page number.
|
||||
page_size (int): The number of agents per page.
|
||||
total_pages (int): The total number of pages.
|
||||
"""
|
||||
|
||||
analytics: list[prisma.models.AnalyticsTracker]
|
||||
total_count: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
class FeaturedAgentResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents a response containing a list of featured agents.
|
||||
|
||||
Attributes:
|
||||
featured_agents (list[FeaturedAgent]): The list of featured agents.
|
||||
total_count (int): The total count of featured agents.
|
||||
page (int): The current page number.
|
||||
page_size (int): The number of agents per page.
|
||||
total_pages (int): The total number of pages.
|
||||
"""
|
||||
|
||||
featured_agents: list[prisma.models.FeaturedAgent]
|
||||
total_count: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
async def delete_agent(agent_id: str) -> prisma.models.Agents | None:
|
||||
"""
|
||||
Delete an agent from the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to delete.
|
||||
|
||||
Returns:
|
||||
prisma.models.Agents | None: The deleted agent if found, None otherwise.
|
||||
|
||||
Raises:
|
||||
AgentQueryError: If there is an error deleting the agent from the database.
|
||||
"""
|
||||
try:
|
||||
deleted_agent = await prisma.models.Agents.prisma().delete(
|
||||
where={"id": agent_id}
|
||||
)
|
||||
return deleted_agent
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def create_agent_entry(
|
||||
name: str,
|
||||
description: str,
|
||||
author: str,
|
||||
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.
|
||||
|
||||
Args:
|
||||
name (str): The name of the agent.
|
||||
description (str): The description of the agent.
|
||||
author (str): The author of the agent.
|
||||
keywords (List[str]): The keywords associated with the agent.
|
||||
categories (List[str]): The categories associated with the agent.
|
||||
graph (dict): The graph data of the agent.
|
||||
|
||||
Returns:
|
||||
dict: The newly created agent entry.
|
||||
|
||||
Raises:
|
||||
AgentQueryError: If there is an error creating the agent entry.
|
||||
"""
|
||||
try:
|
||||
agent = await prisma.models.Agents.prisma().create(
|
||||
data={
|
||||
"name": name,
|
||||
"description": description,
|
||||
"author": author,
|
||||
"keywords": keywords,
|
||||
"categories": categories,
|
||||
"graph": graph,
|
||||
"AnalyticsTracker": {"create": {"downloads": 0, "views": 0}},
|
||||
"submissionStatus": submission_state,
|
||||
}
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
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,
|
||||
) -> prisma.models.Agents | 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,
|
||||
name: str | None = None,
|
||||
keyword: str | None = None,
|
||||
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",
|
||||
):
|
||||
"""
|
||||
Retrieve a list of agents from the database based on the provided filters and pagination parameters.
|
||||
|
||||
Args:
|
||||
page (int, optional): The page number to retrieve. Defaults to 1.
|
||||
page_size (int, optional): The number of agents per page. Defaults to 10.
|
||||
name (str, optional): Filter agents by name. Defaults to None.
|
||||
keyword (str, optional): Filter agents by keyword. Defaults to None.
|
||||
category (str, optional): Filter agents by category. Defaults to None.
|
||||
description (str, optional): Filter agents by description. Defaults to None.
|
||||
description_threshold (int, optional): The minimum fuzzy search threshold for the description. Defaults to 60.
|
||||
sort_by (str, optional): The field to sort the agents by. Defaults to "createdAt".
|
||||
sort_order (str, optional): The sort order ("asc" or "desc"). Defaults to "desc".
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the list of agents, total count, current page number, page size, and total number of pages.
|
||||
"""
|
||||
try:
|
||||
# Define the base query
|
||||
query = {}
|
||||
|
||||
# Add optional filters
|
||||
if name:
|
||||
query["name"] = {"contains": name, "mode": "insensitive"}
|
||||
if keyword:
|
||||
query["keywords"] = {"has": keyword}
|
||||
if category:
|
||||
query["categories"] = {"has": category}
|
||||
|
||||
query["submissionStatus"] = submission_status
|
||||
|
||||
# Define sorting
|
||||
order = {sort_by: sort_order}
|
||||
|
||||
# Calculate pagination
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Execute the query
|
||||
try:
|
||||
agents = await prisma.models.Agents.prisma().find_many(
|
||||
where=query, # type: ignore
|
||||
order=order, # type: ignore
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
|
||||
# Apply fuzzy search on description if provided
|
||||
if description:
|
||||
try:
|
||||
filtered_agents = []
|
||||
for agent in agents:
|
||||
if (
|
||||
agent.description
|
||||
and fuzzywuzzy.fuzz.partial_ratio(
|
||||
description.lower(), agent.description.lower()
|
||||
)
|
||||
>= description_threshold
|
||||
):
|
||||
filtered_agents.append(agent)
|
||||
agents = filtered_agents
|
||||
except AttributeError as e:
|
||||
raise AgentQueryError(f"Error during fuzzy search: {str(e)}")
|
||||
|
||||
# Get total count for pagination info
|
||||
total_count = len(agents)
|
||||
|
||||
return {
|
||||
"agents": agents,
|
||||
"total_count": total_count,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total_count + page_size - 1) // page_size,
|
||||
}
|
||||
|
||||
except AgentQueryError as e:
|
||||
# Log the error or handle it as needed
|
||||
raise e
|
||||
except ValueError as e:
|
||||
raise AgentQueryError(f"Invalid input parameter: {str(e)}")
|
||||
except Exception as e:
|
||||
# Catch any other unexpected exceptions
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_agent_details(agent_id: str, version: int | None = None):
|
||||
"""
|
||||
Retrieve agent details from the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int | None, optional): The version of the agent. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: The agent details.
|
||||
|
||||
Raises:
|
||||
AgentQueryError: If the agent is not found or if there is an error querying the database.
|
||||
"""
|
||||
try:
|
||||
query = {"id": agent_id}
|
||||
if version is not None:
|
||||
query["version"] = version # type: ignore
|
||||
|
||||
agent = await prisma.models.Agents.prisma().find_first(where=query) # type: ignore
|
||||
|
||||
if not agent:
|
||||
raise AgentQueryError("Agent not found")
|
||||
|
||||
return agent
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def search_db(
|
||||
query: str,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
categories: typing.List[str] | None = None,
|
||||
description_threshold: int = 60,
|
||||
sort_by: str = "rank",
|
||||
sort_order: typing.Literal["desc"] | typing.Literal["asc"] = "desc",
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
) -> market.model.ListResponse[market.utils.extension_types.AgentsWithRank]:
|
||||
"""Perform a search for agents based on the provided query string.
|
||||
|
||||
Args:
|
||||
query (str): the search string
|
||||
page (int, optional): page for searching. Defaults to 1.
|
||||
page_size (int, optional): the number of results to return. Defaults to 10.
|
||||
categories (List[str] | None, optional): list of category filters. Defaults to None.
|
||||
description_threshold (int, optional): number of characters to return. Defaults to 60.
|
||||
sort_by (str, optional): sort by option. Defaults to "rank".
|
||||
sort_order ("asc" | "desc", optional): the sort order. Defaults to "desc".
|
||||
|
||||
Raises:
|
||||
AgentQueryError: Raises an error if the query fails.
|
||||
AgentQueryError: Raises if an unexpected error occurs.
|
||||
|
||||
Returns:
|
||||
List[AgentsWithRank]: List of agents matching the search criteria.
|
||||
"""
|
||||
try:
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
category_filter = "1=1"
|
||||
if categories:
|
||||
category_conditions = [f"'{cat}' = ANY(categories)" for cat in categories]
|
||||
category_filter = "AND (" + " OR ".join(category_conditions) + ")"
|
||||
|
||||
# Construct the ORDER BY clause based on the sort_by parameter
|
||||
if sort_by in ["createdAt", "updatedAt"]:
|
||||
order_by_clause = f'"{sort_by}" {sort_order.upper()}, rank DESC'
|
||||
elif sort_by == "name":
|
||||
order_by_clause = f"name {sort_order.upper()}, rank DESC"
|
||||
else:
|
||||
order_by_clause = 'rank DESC, "createdAt" DESC'
|
||||
|
||||
submission_status_filter = f""""submissionStatus" = '{submission_status}'"""
|
||||
|
||||
sql_query = f"""
|
||||
WITH query AS (
|
||||
SELECT to_tsquery(string_agg(lexeme || ':*', ' & ' ORDER BY positions)) AS q
|
||||
FROM unnest(to_tsvector('{query}'))
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
version,
|
||||
name,
|
||||
LEFT(description, {description_threshold}) AS description,
|
||||
author,
|
||||
keywords,
|
||||
categories,
|
||||
graph,
|
||||
"submissionStatus",
|
||||
"submissionDate",
|
||||
CASE
|
||||
WHEN query.q::text = '' THEN 1.0
|
||||
ELSE COALESCE(ts_rank(CAST(search AS tsvector), query.q), 0.0)
|
||||
END AS rank
|
||||
FROM market."Agents", query
|
||||
WHERE
|
||||
(query.q::text = '' OR search @@ query.q)
|
||||
AND {category_filter}
|
||||
AND {submission_status_filter}
|
||||
ORDER BY {order_by_clause}
|
||||
LIMIT {page_size}
|
||||
OFFSET {offset};
|
||||
"""
|
||||
|
||||
results = await prisma.client.get_client().query_raw(
|
||||
query=sql_query,
|
||||
model=market.utils.extension_types.AgentsWithRank,
|
||||
)
|
||||
|
||||
class CountResponse(pydantic.BaseModel):
|
||||
count: int
|
||||
|
||||
count_query = f"""
|
||||
WITH query AS (
|
||||
SELECT to_tsquery(string_agg(lexeme || ':*', ' & ' ORDER BY positions)) AS q
|
||||
FROM unnest(to_tsvector('{query}'))
|
||||
)
|
||||
SELECT COUNT(*)
|
||||
FROM market."Agents", query
|
||||
WHERE (search @@ query.q OR query.q = '') AND {category_filter} AND {submission_status_filter};
|
||||
"""
|
||||
|
||||
total_count = await prisma.client.get_client().query_first(
|
||||
query=count_query,
|
||||
model=CountResponse,
|
||||
)
|
||||
total_count = total_count.count if total_count else 0
|
||||
|
||||
return market.model.ListResponse(
|
||||
items=results,
|
||||
total_count=total_count,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=(total_count + page_size - 1) // page_size,
|
||||
)
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_top_agents_by_downloads(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
) -> market.model.ListResponse[prisma.models.AnalyticsTracker]:
|
||||
"""Retrieve the top agents by download count.
|
||||
|
||||
Args:
|
||||
page (int, optional): The page number. Defaults to 1.
|
||||
page_size (int, optional): The number of agents per page. Defaults to 10.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the list of agents, total count, current page number, page size, and total number of pages.
|
||||
"""
|
||||
try:
|
||||
# Calculate pagination
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Execute the query
|
||||
try:
|
||||
# Agents with no downloads will not be included in the results... is this the desired behavior?
|
||||
analytics = await prisma.models.AnalyticsTracker.prisma().find_many(
|
||||
include={"agent": True},
|
||||
order={"downloads": "desc"},
|
||||
where={"agent": {"is": {"submissionStatus": submission_status}}},
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
|
||||
try:
|
||||
total_count = await prisma.models.AnalyticsTracker.prisma().count(
|
||||
where={"agent": {"is": {"submissionStatus": submission_status}}},
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
|
||||
return market.model.ListResponse(
|
||||
items=analytics,
|
||||
total_count=total_count,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=(total_count + page_size - 1) // page_size,
|
||||
)
|
||||
|
||||
except AgentQueryError as e:
|
||||
# Log the error or handle it as needed
|
||||
raise e from e
|
||||
except ValueError as e:
|
||||
raise AgentQueryError(f"Invalid input parameter: {str(e)}") from e
|
||||
except Exception as e:
|
||||
# Catch any other unexpected exceptions
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}") from e
|
||||
|
||||
|
||||
async def set_agent_featured(
|
||||
agent_id: str, is_active: bool = True, featured_categories: list[str] = ["featured"]
|
||||
) -> prisma.models.FeaturedAgent:
|
||||
"""Set an agent as featured in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
category (str, optional): The category to set the agent as featured. Defaults to "featured".
|
||||
|
||||
Raises:
|
||||
AgentQueryError: If there is an error setting the agent as featured.
|
||||
"""
|
||||
try:
|
||||
agent = await prisma.models.Agents.prisma().find_unique(where={"id": agent_id})
|
||||
if not agent:
|
||||
raise AgentQueryError(f"Agent with ID {agent_id} not found.")
|
||||
|
||||
featured = await prisma.models.FeaturedAgent.prisma().upsert(
|
||||
where={"agentId": agent_id},
|
||||
data={
|
||||
"update": {
|
||||
"featuredCategories": featured_categories,
|
||||
"isActive": is_active,
|
||||
},
|
||||
"create": {
|
||||
"featuredCategories": featured_categories,
|
||||
"isActive": is_active,
|
||||
"agent": {"connect": {"id": agent_id}},
|
||||
},
|
||||
},
|
||||
)
|
||||
return featured
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_featured_agents(
|
||||
category: str = "featured",
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
|
||||
) -> FeaturedAgentResponse:
|
||||
"""Retrieve a list of featured agents from the database based on the provided category.
|
||||
|
||||
Args:
|
||||
category (str, optional): The category of featured agents to retrieve. Defaults to "featured".
|
||||
page (int, optional): The page number to retrieve. Defaults to 1.
|
||||
page_size (int, optional): The number of agents per page. Defaults to 10.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the list of featured agents, total count, current page number, page size, and total number of pages.
|
||||
"""
|
||||
try:
|
||||
# Calculate pagination
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Execute the query
|
||||
try:
|
||||
featured_agents = await prisma.models.FeaturedAgent.prisma().find_many(
|
||||
where={
|
||||
"featuredCategories": {"has": category},
|
||||
"isActive": True,
|
||||
"agent": {"is": {"submissionStatus": submission_status}},
|
||||
},
|
||||
include={"agent": {"include": {"AnalyticsTracker": True}}},
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
|
||||
# Get total count for pagination info
|
||||
total_count = len(featured_agents)
|
||||
|
||||
return FeaturedAgentResponse(
|
||||
featured_agents=featured_agents,
|
||||
total_count=total_count,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=(total_count + page_size - 1) // page_size,
|
||||
)
|
||||
|
||||
except AgentQueryError as e:
|
||||
# Log the error or handle it as needed
|
||||
raise e from e
|
||||
except ValueError as e:
|
||||
raise AgentQueryError(f"Invalid input parameter: {str(e)}") from e
|
||||
except Exception as e:
|
||||
# Catch any other unexpected exceptions
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}") from e
|
||||
|
||||
|
||||
async def remove_featured_category(
|
||||
agent_id: str, category: str
|
||||
) -> prisma.models.FeaturedAgent | None:
|
||||
"""Adds a featured category to an agent.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
category (str): The category to add to the agent.
|
||||
|
||||
Returns:
|
||||
FeaturedAgentResponse: The updated list of featured agents.
|
||||
"""
|
||||
try:
|
||||
# get the existing categories
|
||||
featured_agent = await prisma.models.FeaturedAgent.prisma().find_unique(
|
||||
where={"agentId": agent_id},
|
||||
include={"agent": True},
|
||||
)
|
||||
|
||||
if not featured_agent:
|
||||
raise AgentQueryError(f"Agent with ID {agent_id} not found.")
|
||||
|
||||
# remove the category from the list
|
||||
featured_agent.featuredCategories.remove(category)
|
||||
|
||||
featured_agent = await prisma.models.FeaturedAgent.prisma().update(
|
||||
where={"agentId": agent_id},
|
||||
data={"featuredCategories": featured_agent.featuredCategories},
|
||||
)
|
||||
|
||||
return featured_agent
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def add_featured_category(
|
||||
agent_id: str, category: str
|
||||
) -> prisma.models.FeaturedAgent | None:
|
||||
"""Removes a featured category from an agent.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
category (str): The category to remove from the agent.
|
||||
|
||||
Returns:
|
||||
FeaturedAgentResponse: The updated list of featured agents.
|
||||
"""
|
||||
try:
|
||||
featured_agent = await prisma.models.FeaturedAgent.prisma().update(
|
||||
where={"agentId": agent_id},
|
||||
data={"featuredCategories": {"push": [category]}},
|
||||
)
|
||||
|
||||
return featured_agent
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_agent_featured(agent_id: str) -> prisma.models.FeaturedAgent | None:
|
||||
"""Retrieve an agent's featured categories from the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
|
||||
Returns:
|
||||
FeaturedAgentResponse: The list of featured agents.
|
||||
"""
|
||||
try:
|
||||
featured_agent = await prisma.models.FeaturedAgent.prisma().find_unique(
|
||||
where={"agentId": agent_id},
|
||||
)
|
||||
return featured_agent
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_not_featured_agents(
|
||||
page: int = 1, page_size: int = 10
|
||||
) -> typing.List[prisma.models.Agents]:
|
||||
"""
|
||||
Retrieve a list of not featured agents from the database.
|
||||
"""
|
||||
try:
|
||||
agents = await prisma.client.get_client().query_raw(
|
||||
query=f"""
|
||||
SELECT
|
||||
"market"."Agents".id,
|
||||
"market"."Agents"."createdAt",
|
||||
"market"."Agents"."updatedAt",
|
||||
"market"."Agents".version,
|
||||
"market"."Agents".name,
|
||||
LEFT("market"."Agents".description, 500) AS description,
|
||||
"market"."Agents".author,
|
||||
"market"."Agents".keywords,
|
||||
"market"."Agents".categories,
|
||||
"market"."Agents".graph,
|
||||
"market"."Agents"."submissionStatus",
|
||||
"market"."Agents"."submissionDate",
|
||||
"market"."Agents".search::text AS search
|
||||
FROM "market"."Agents"
|
||||
LEFT JOIN "market"."FeaturedAgent" ON "market"."Agents"."id" = "market"."FeaturedAgent"."agentId"
|
||||
WHERE ("market"."FeaturedAgent"."agentId" IS NULL OR "market"."FeaturedAgent"."featuredCategories" = '{{}}')
|
||||
AND "market"."Agents"."submissionStatus" = 'APPROVED'
|
||||
ORDER BY "market"."Agents"."createdAt" DESC
|
||||
LIMIT {page_size} OFFSET {page_size * (page - 1)}
|
||||
""",
|
||||
model=prisma.models.Agents,
|
||||
)
|
||||
return agents
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def get_all_categories() -> market.model.CategoriesResponse:
|
||||
"""
|
||||
Retrieve all unique categories from the database.
|
||||
|
||||
Returns:
|
||||
CategoriesResponse: A list of unique categories.
|
||||
"""
|
||||
try:
|
||||
agents = await prisma.models.Agents.prisma().find_many(distinct=["categories"])
|
||||
|
||||
# Aggregate categories on the Python side
|
||||
all_categories = set()
|
||||
for agent in agents:
|
||||
all_categories.update(agent.categories)
|
||||
|
||||
unique_categories = sorted(list(all_categories))
|
||||
|
||||
return market.model.CategoriesResponse(unique_categories=unique_categories)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception:
|
||||
# Return an empty list of categories in case of unexpected errors
|
||||
return market.model.CategoriesResponse(unique_categories=[])
|
||||
|
||||
|
||||
async def create_agent_installed_event(
|
||||
event_data: market.model.AgentInstalledFromMarketplaceEventData,
|
||||
):
|
||||
try:
|
||||
await prisma.models.InstallTracker.prisma().create(
|
||||
data={
|
||||
"installedAgentId": event_data.installed_agent_id,
|
||||
"marketplaceAgentId": event_data.marketplace_agent_id,
|
||||
"installationLocation": prisma.enums.InstallationLocation(
|
||||
event_data.installation_location.name
|
||||
),
|
||||
}
|
||||
)
|
||||
except prisma.errors.PrismaError as e:
|
||||
raise AgentQueryError(f"Database query failed: {str(e)}")
|
||||
except Exception as e:
|
||||
raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
|
@ -1,161 +0,0 @@
|
||||
import datetime
|
||||
import typing
|
||||
from enum import Enum
|
||||
from typing import Generic, Literal, TypeVar, Union
|
||||
|
||||
import prisma.enums
|
||||
import pydantic
|
||||
|
||||
|
||||
class InstallationLocation(str, Enum):
|
||||
LOCAL = "local"
|
||||
CLOUD = "cloud"
|
||||
|
||||
|
||||
class AgentInstalledFromMarketplaceEventData(pydantic.BaseModel):
|
||||
marketplace_agent_id: str
|
||||
installed_agent_id: str
|
||||
installation_location: InstallationLocation
|
||||
|
||||
|
||||
class AgentInstalledFromTemplateEventData(pydantic.BaseModel):
|
||||
template_id: str
|
||||
installed_agent_id: str
|
||||
installation_location: InstallationLocation
|
||||
|
||||
|
||||
class AgentInstalledFromMarketplaceEvent(pydantic.BaseModel):
|
||||
event_name: Literal["agent_installed_from_marketplace"]
|
||||
event_data: AgentInstalledFromMarketplaceEventData
|
||||
|
||||
|
||||
class AgentInstalledFromTemplateEvent(pydantic.BaseModel):
|
||||
event_name: Literal["agent_installed_from_template"]
|
||||
event_data: AgentInstalledFromTemplateEventData
|
||||
|
||||
|
||||
AnalyticsEvent = Union[
|
||||
AgentInstalledFromMarketplaceEvent, AgentInstalledFromTemplateEvent
|
||||
]
|
||||
|
||||
|
||||
class AnalyticsRequest(pydantic.BaseModel):
|
||||
event: AnalyticsEvent
|
||||
|
||||
|
||||
class AddAgentRequest(pydantic.BaseModel):
|
||||
graph: dict[str, typing.Any]
|
||||
author: str
|
||||
keywords: list[str]
|
||||
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.
|
||||
|
||||
Attributes:
|
||||
id (str): The ID of the agent.
|
||||
name (str, optional): The name of the agent.
|
||||
description (str, optional): The description of the agent.
|
||||
author (str, optional): The author of the agent.
|
||||
keywords (list[str]): The keywords associated with the agent.
|
||||
categories (list[str]): The categories the agent belongs to.
|
||||
version (int): The version of the agent.
|
||||
createdAt (str): The creation date of the agent.
|
||||
updatedAt (str): The last update date of the agent.
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: typing.Optional[str]
|
||||
description: typing.Optional[str]
|
||||
author: typing.Optional[str]
|
||||
keywords: list[str]
|
||||
categories: list[str]
|
||||
version: int
|
||||
createdAt: datetime.datetime
|
||||
updatedAt: datetime.datetime
|
||||
submissionStatus: str
|
||||
views: int = 0
|
||||
downloads: int = 0
|
||||
|
||||
|
||||
class AgentDetailResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents the response data for an agent detail.
|
||||
|
||||
Attributes:
|
||||
id (str): The ID of the agent.
|
||||
name (Optional[str]): The name of the agent.
|
||||
description (Optional[str]): The description of the agent.
|
||||
author (Optional[str]): The author of the agent.
|
||||
keywords (List[str]): The keywords associated with the agent.
|
||||
categories (List[str]): The categories the agent belongs to.
|
||||
version (int): The version of the agent.
|
||||
createdAt (str): The creation date of the agent.
|
||||
updatedAt (str): The last update date of the agent.
|
||||
graph (Dict[str, Any]): The graph data of the agent.
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: typing.Optional[str]
|
||||
description: typing.Optional[str]
|
||||
author: typing.Optional[str]
|
||||
keywords: list[str]
|
||||
categories: list[str]
|
||||
version: int
|
||||
createdAt: datetime.datetime
|
||||
updatedAt: datetime.datetime
|
||||
graph: dict[str, typing.Any]
|
||||
|
||||
|
||||
class FeaturedAgentResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents the response data for an agent detail.
|
||||
"""
|
||||
|
||||
agentId: str
|
||||
featuredCategories: list[str]
|
||||
createdAt: datetime.datetime
|
||||
updatedAt: datetime.datetime
|
||||
isActive: bool
|
||||
|
||||
|
||||
class CategoriesResponse(pydantic.BaseModel):
|
||||
"""
|
||||
Represents the response data for a list of categories.
|
||||
|
||||
Attributes:
|
||||
unique_categories (list[str]): The list of unique categories.
|
||||
"""
|
||||
|
||||
unique_categories: list[str]
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ListResponse(pydantic.BaseModel, Generic[T]):
|
||||
"""
|
||||
Represents a list response.
|
||||
|
||||
Attributes:
|
||||
items (list[T]): The list of items.
|
||||
total_count (int): The total count of items.
|
||||
page (int): The current page number.
|
||||
page_size (int): The number of items per page.
|
||||
total_pages (int): The total number of pages.
|
||||
"""
|
||||
|
||||
items: list[T]
|
||||
total_count: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
@ -1,286 +0,0 @@
|
||||
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()
|
||||
|
||||
|
||||
@router.delete("/agent/{agent_id}", response_model=market.model.AgentResponse)
|
||||
async def delete_agent(
|
||||
agent_id: str,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
):
|
||||
"""
|
||||
Delete an agent and all related records from the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to delete.
|
||||
|
||||
Returns:
|
||||
market.model.AgentResponse: The deleted agent's data.
|
||||
|
||||
Raises:
|
||||
fastapi.HTTPException: If the agent is not found or if there's an error during deletion.
|
||||
"""
|
||||
try:
|
||||
deleted_agent = await market.db.delete_agent(agent_id)
|
||||
if deleted_agent:
|
||||
return market.model.AgentResponse(**deleted_agent.dict())
|
||||
else:
|
||||
raise fastapi.HTTPException(status_code=404, detail="Agent not found")
|
||||
except market.db.AgentQueryError as e:
|
||||
logger.error(f"Error deleting agent: {e}")
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error deleting agent: {e}")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail="An unexpected error occurred"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/agent", response_model=market.model.AgentResponse)
|
||||
async def create_agent_entry(
|
||||
request: market.model.AddAgentRequest,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_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))
|
||||
|
||||
|
||||
@router.post("/agent/featured/{agent_id}")
|
||||
async def set_agent_featured(
|
||||
agent_id: str,
|
||||
categories: list[str] = fastapi.Query(
|
||||
default=["featured"],
|
||||
description="The categories to set the agent as featured in",
|
||||
),
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.FeaturedAgentResponse:
|
||||
"""
|
||||
A basic endpoint to set an agent as featured in the database.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.set_agent_featured(
|
||||
agent_id, is_active=True, featured_categories=categories
|
||||
)
|
||||
return market.model.FeaturedAgentResponse(**agent.model_dump())
|
||||
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))
|
||||
|
||||
|
||||
@router.get("/agent/featured/{agent_id}")
|
||||
async def get_agent_featured(
|
||||
agent_id: str,
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.FeaturedAgentResponse | None:
|
||||
"""
|
||||
A basic endpoint to get an agent as featured in the database.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.get_agent_featured(agent_id)
|
||||
if agent:
|
||||
return market.model.FeaturedAgentResponse(**agent.model_dump())
|
||||
else:
|
||||
return None
|
||||
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))
|
||||
|
||||
|
||||
@router.delete("/agent/featured/{agent_id}")
|
||||
async def unset_agent_featured(
|
||||
agent_id: str,
|
||||
category: str = "featured",
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.FeaturedAgentResponse | None:
|
||||
"""
|
||||
A basic endpoint to unset an agent as featured in the database.
|
||||
"""
|
||||
try:
|
||||
featured = await market.db.remove_featured_category(agent_id, category=category)
|
||||
if featured:
|
||||
return market.model.FeaturedAgentResponse(**featured.model_dump())
|
||||
else:
|
||||
return None
|
||||
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))
|
||||
|
||||
|
||||
@router.get("/agent/not-featured")
|
||||
async def get_not_featured_agents(
|
||||
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"
|
||||
),
|
||||
user: autogpt_libs.auth.User = fastapi.Depends(
|
||||
autogpt_libs.auth.requires_admin_user
|
||||
),
|
||||
) -> market.model.ListResponse[market.model.AgentResponse]:
|
||||
"""
|
||||
A basic endpoint to get all not featured agents in the database.
|
||||
"""
|
||||
try:
|
||||
agents = await market.db.get_not_featured_agents(page=page, page_size=page_size)
|
||||
return market.model.ListResponse(
|
||||
items=[
|
||||
market.model.AgentResponse(**agent.model_dump()) for agent in agents
|
||||
],
|
||||
total_count=len(agents),
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=999,
|
||||
)
|
||||
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))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent/submissions",
|
||||
response_model=market.model.ListResponse[market.model.AgentResponse],
|
||||
)
|
||||
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
|
||||
),
|
||||
) -> market.model.ListResponse[market.model.AgentResponse]:
|
||||
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.ListResponse(
|
||||
items=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
|
||||
),
|
||||
) -> prisma.models.Agents | None:
|
||||
"""
|
||||
A basic endpoint to review a submission in the database.
|
||||
"""
|
||||
logger.info(
|
||||
f"Reviewing submission: {review_request.agent_id}, {review_request.version}"
|
||||
)
|
||||
try:
|
||||
agent = 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 agent
|
||||
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))
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def get_categories() -> market.model.CategoriesResponse:
|
||||
"""
|
||||
A basic endpoint to get all available categories.
|
||||
"""
|
||||
try:
|
||||
categories = await market.db.get_all_categories()
|
||||
return categories
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(status_code=500, detail=str(e))
|
@ -1,76 +0,0 @@
|
||||
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
|
@ -1,368 +0,0 @@
|
||||
import json
|
||||
import tempfile
|
||||
import typing
|
||||
|
||||
import fastapi
|
||||
import fastapi.responses
|
||||
import prisma
|
||||
import prisma.enums
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
import market.utils.analytics
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agents", response_model=market.model.ListResponse[market.model.AgentResponse]
|
||||
)
|
||||
async def list_agents(
|
||||
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)"
|
||||
),
|
||||
submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
|
||||
default=prisma.enums.SubmissionStatus.APPROVED,
|
||||
description="Filter by submission status",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Retrieve a list of agents based on the provided filters.
|
||||
|
||||
Args:
|
||||
page (int): Page number (default: 1).
|
||||
page_size (int): Number of items per page (default: 10, min: 1, max: 100).
|
||||
name (str, optional): Filter by agent name.
|
||||
keyword (str, optional): Filter by keyword.
|
||||
category (str, optional): Filter by category.
|
||||
description (str, optional): Fuzzy search in description.
|
||||
description_threshold (int): Fuzzy search threshold (default: 60, min: 0, max: 100).
|
||||
sort_by (str): Field to sort by (default: "createdAt").
|
||||
sort_order (str): Sort order (asc or desc) (default: "desc").
|
||||
submission_status (str): Filter by submission status (default: "APPROVED").
|
||||
|
||||
Returns:
|
||||
market.model.ListResponse[market.model.AgentResponse]: A response containing the list of agents and pagination information.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
|
||||
"""
|
||||
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=submission_status,
|
||||
)
|
||||
|
||||
agents = [
|
||||
market.model.AgentResponse(**agent.dict()) for agent in result["agents"]
|
||||
]
|
||||
|
||||
return market.model.ListResponse(
|
||||
items=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:
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{agent_id}", response_model=market.model.AgentDetailResponse)
|
||||
async def get_agent_details_endpoint(
|
||||
background_tasks: fastapi.BackgroundTasks,
|
||||
agent_id: str = fastapi.Path(..., description="The ID of the agent to retrieve"),
|
||||
version: typing.Optional[int] = fastapi.Query(
|
||||
None, description="Specific version of the agent"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Retrieve details of a specific agent.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to retrieve.
|
||||
version (Optional[int]): Specific version of the agent (default: None).
|
||||
|
||||
Returns:
|
||||
market.model.AgentDetailResponse: The response containing the agent details.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the agent is not found or an unexpected error occurs.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.get_agent_details(agent_id, version)
|
||||
background_tasks.add_task(market.utils.analytics.track_view, agent_id)
|
||||
return market.model.AgentDetailResponse(**agent.model_dump())
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{agent_id}/download")
|
||||
async def download_agent(
|
||||
background_tasks: fastapi.BackgroundTasks,
|
||||
agent_id: str = fastapi.Path(..., description="The ID of the agent to retrieve"),
|
||||
version: typing.Optional[int] = fastapi.Query(
|
||||
None, description="Specific version of the agent"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Download details of a specific agent.
|
||||
|
||||
NOTE: This is the same as agent details, however it also triggers
|
||||
the "download" tracking. We don't actually want to download a file though
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to retrieve.
|
||||
version (Optional[int]): Specific version of the agent (default: None).
|
||||
|
||||
Returns:
|
||||
market.model.AgentDetailResponse: The response containing the agent details.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the agent is not found or an unexpected error occurs.
|
||||
"""
|
||||
try:
|
||||
agent = await market.db.get_agent_details(agent_id, version)
|
||||
background_tasks.add_task(market.utils.analytics.track_download, agent_id)
|
||||
return market.model.AgentDetailResponse(**agent.model_dump())
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{agent_id}/download-file")
|
||||
async def download_agent_file(
|
||||
background_tasks: fastapi.BackgroundTasks,
|
||||
agent_id: str = fastapi.Path(..., description="The ID of the agent to download"),
|
||||
version: typing.Optional[int] = fastapi.Query(
|
||||
None, description="Specific version of the agent"
|
||||
),
|
||||
) -> fastapi.responses.FileResponse:
|
||||
"""
|
||||
Download the agent file by streaming its content.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to download.
|
||||
version (Optional[int]): Specific version of the agent to download.
|
||||
|
||||
Returns:
|
||||
StreamingResponse: A streaming response containing the agent's graph data.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the agent is not found or an unexpected error occurs.
|
||||
"""
|
||||
agent = await market.db.get_agent_details(agent_id, version)
|
||||
|
||||
graph_data: prisma.Json = agent.graph
|
||||
|
||||
background_tasks.add_task(market.utils.analytics.track_download, agent_id)
|
||||
|
||||
file_name = f"agent_{agent_id}_v{version or 'latest'}.json"
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False
|
||||
) as tmp_file:
|
||||
tmp_file.write(json.dumps(graph_data))
|
||||
tmp_file.flush()
|
||||
|
||||
return fastapi.responses.FileResponse(
|
||||
tmp_file.name, filename=file_name, media_type="application/json"
|
||||
)
|
||||
|
||||
|
||||
# top agents by downloads
|
||||
@router.get(
|
||||
"/top-downloads/agents",
|
||||
response_model=market.model.ListResponse[market.model.AgentResponse],
|
||||
)
|
||||
async def top_agents_by_downloads(
|
||||
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"
|
||||
),
|
||||
submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
|
||||
default=prisma.enums.SubmissionStatus.APPROVED,
|
||||
description="Filter by submission status",
|
||||
),
|
||||
) -> market.model.ListResponse[market.model.AgentResponse]:
|
||||
"""
|
||||
Retrieve a list of top agents based on the number of downloads.
|
||||
|
||||
Args:
|
||||
page (int): Page number (default: 1).
|
||||
page_size (int): Number of items per page (default: 10, min: 1, max: 100).
|
||||
submission_status (str): Filter by submission status (default: "APPROVED").
|
||||
|
||||
Returns:
|
||||
market.model.ListResponse[market.model.AgentResponse]: A response containing the list of top agents and pagination information.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
|
||||
"""
|
||||
try:
|
||||
result = await market.db.get_top_agents_by_downloads(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
submission_status=submission_status,
|
||||
)
|
||||
|
||||
ret = market.model.ListResponse(
|
||||
total_count=result.total_count,
|
||||
page=result.page,
|
||||
page_size=result.page_size,
|
||||
total_pages=result.total_pages,
|
||||
items=[
|
||||
market.model.AgentResponse(
|
||||
id=item.agent.id,
|
||||
name=item.agent.name,
|
||||
description=item.agent.description,
|
||||
author=item.agent.author,
|
||||
keywords=item.agent.keywords,
|
||||
categories=item.agent.categories,
|
||||
version=item.agent.version,
|
||||
createdAt=item.agent.createdAt,
|
||||
updatedAt=item.agent.updatedAt,
|
||||
views=item.views,
|
||||
downloads=item.downloads,
|
||||
submissionStatus=item.agent.submissionStatus,
|
||||
)
|
||||
for item in result.items
|
||||
if item.agent is not None
|
||||
],
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get(
|
||||
"/featured/agents",
|
||||
response_model=market.model.ListResponse[market.model.AgentResponse],
|
||||
)
|
||||
async def get_featured_agents(
|
||||
category: str = fastapi.Query(
|
||||
"featured", description="Category of featured agents"
|
||||
),
|
||||
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"
|
||||
),
|
||||
submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
|
||||
default=prisma.enums.SubmissionStatus.APPROVED,
|
||||
description="Filter by submission status",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Retrieve a list of featured agents based on the provided category.
|
||||
|
||||
Args:
|
||||
category (str): Category of featured agents (default: "featured").
|
||||
page (int): Page number (default: 1).
|
||||
page_size (int): Number of items per page (default: 10, min: 1, max: 100).
|
||||
submission_status (str): Filter by submission status (default: "APPROVED").
|
||||
|
||||
Returns:
|
||||
market.model.ListResponse[market.model.AgentResponse]: A response containing the list of featured agents and pagination information.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
|
||||
"""
|
||||
try:
|
||||
result = await market.db.get_featured_agents(
|
||||
category=category,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
submission_status=submission_status,
|
||||
)
|
||||
|
||||
ret = market.model.ListResponse(
|
||||
total_count=result.total_count,
|
||||
page=result.page,
|
||||
page_size=result.page_size,
|
||||
total_pages=result.total_pages,
|
||||
items=[
|
||||
market.model.AgentResponse(
|
||||
id=item.agent.id,
|
||||
name=item.agent.name,
|
||||
description=item.agent.description,
|
||||
author=item.agent.author,
|
||||
keywords=item.agent.keywords,
|
||||
categories=item.agent.categories,
|
||||
version=item.agent.version,
|
||||
createdAt=item.agent.createdAt,
|
||||
updatedAt=item.agent.updatedAt,
|
||||
views=(
|
||||
item.agent.AnalyticsTracker[0].views
|
||||
if item.agent.AnalyticsTracker
|
||||
and len(item.agent.AnalyticsTracker) > 0
|
||||
else 0
|
||||
),
|
||||
downloads=(
|
||||
item.agent.AnalyticsTracker[0].downloads
|
||||
if item.agent.AnalyticsTracker
|
||||
and len(item.agent.AnalyticsTracker) > 0
|
||||
else 0
|
||||
),
|
||||
submissionStatus=item.agent.submissionStatus,
|
||||
)
|
||||
for item in result.featured_agents
|
||||
if item.agent is not None
|
||||
],
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
) from e
|
@ -1,26 +0,0 @@
|
||||
import fastapi
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.post("/agent-installed")
|
||||
async def agent_installed_endpoint(
|
||||
event_data: market.model.AgentInstalledFromMarketplaceEventData,
|
||||
):
|
||||
"""
|
||||
Endpoint to track agent installation events from the marketplace.
|
||||
|
||||
Args:
|
||||
event_data (market.model.AgentInstalledFromMarketplaceEventData): The event data.
|
||||
"""
|
||||
try:
|
||||
await market.db.create_agent_installed_event(event_data)
|
||||
except market.db.AgentQueryError as e:
|
||||
raise fastapi.HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"An unexpected error occurred: {e}"
|
||||
)
|
@ -1,56 +0,0 @@
|
||||
import typing
|
||||
|
||||
import fastapi
|
||||
import prisma.enums
|
||||
|
||||
import market.db
|
||||
import market.model
|
||||
import market.utils.extension_types
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.get("/search")
|
||||
async def search(
|
||||
query: str,
|
||||
page: int = fastapi.Query(1, description="The pagination page to start on"),
|
||||
page_size: int = fastapi.Query(
|
||||
10, description="The number of items to return per page"
|
||||
),
|
||||
categories: typing.List[str] = fastapi.Query(
|
||||
None, description="The categories to filter by"
|
||||
),
|
||||
description_threshold: int = fastapi.Query(
|
||||
60, description="The number of characters to return from the description"
|
||||
),
|
||||
sort_by: str = fastapi.Query("rank", description="Sorting by column"),
|
||||
sort_order: typing.Literal["desc", "asc"] = fastapi.Query(
|
||||
"desc", description="The sort order based on sort_by"
|
||||
),
|
||||
submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
|
||||
prisma.enums.SubmissionStatus.APPROVED,
|
||||
description="The submission status to filter by",
|
||||
),
|
||||
) -> market.model.ListResponse[market.utils.extension_types.AgentsWithRank]:
|
||||
"""searches endpoint for agents
|
||||
|
||||
Args:
|
||||
query (str): the search query
|
||||
page (int, optional): the pagination page to start on. Defaults to 1.
|
||||
page_size (int, optional): the number of items to return per page. Defaults to 10.
|
||||
category (str | None, optional): the agent category to filter by. None is no filter. Defaults to None.
|
||||
description_threshold (int, optional): the number of characters to return from the description. Defaults to 60.
|
||||
sort_by (str, optional): Sorting by column. Defaults to "rank".
|
||||
sort_order ('asc' | 'desc', optional): the sort order based on sort_by. Defaults to "desc".
|
||||
"""
|
||||
agents = await market.db.search_db(
|
||||
query=query,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
categories=categories,
|
||||
description_threshold=description_threshold,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
submission_status=submission_status,
|
||||
)
|
||||
return agents
|
@ -1,35 +0,0 @@
|
||||
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))
|
@ -1,47 +0,0 @@
|
||||
import prisma.models
|
||||
|
||||
|
||||
async def track_download(agent_id: str):
|
||||
"""
|
||||
Track the download event in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int | None, optional): The version of the agent. Defaults to None.
|
||||
|
||||
Raises:
|
||||
Exception: If there is an error tracking the download event.
|
||||
"""
|
||||
try:
|
||||
await prisma.models.AnalyticsTracker.prisma().upsert(
|
||||
where={"agentId": agent_id},
|
||||
data={
|
||||
"update": {"downloads": {"increment": 1}},
|
||||
"create": {"agentId": agent_id, "downloads": 1, "views": 0},
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error tracking download event: {str(e)}")
|
||||
|
||||
|
||||
async def track_view(agent_id: str):
|
||||
"""
|
||||
Track the view event in the database.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
version (int | None, optional): The version of the agent. Defaults to None.
|
||||
|
||||
Raises:
|
||||
Exception: If there is an error tracking the view event.
|
||||
"""
|
||||
try:
|
||||
await prisma.models.AnalyticsTracker.prisma().upsert(
|
||||
where={"agentId": agent_id},
|
||||
data={
|
||||
"update": {"views": {"increment": 1}},
|
||||
"create": {"agentId": agent_id, "downloads": 0, "views": 1},
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error tracking view event: {str(e)}")
|
@ -1,5 +0,0 @@
|
||||
import prisma.models
|
||||
|
||||
|
||||
class AgentsWithRank(prisma.models.Agents):
|
||||
rank: float
|
@ -1,6 +0,0 @@
|
||||
import prisma.models
|
||||
|
||||
prisma.models.Agents.create_partial(
|
||||
"AgentOnlyDescriptionNameAuthorIdCategories",
|
||||
include={"name", "author", "id", "categories"},
|
||||
)
|
@ -1,61 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Agents" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"name" TEXT,
|
||||
"description" TEXT,
|
||||
"author" TEXT,
|
||||
"keywords" TEXT[],
|
||||
"categories" TEXT[],
|
||||
"search" tsvector DEFAULT ''::tsvector,
|
||||
"graph" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Agents_pkey" PRIMARY KEY ("id","version")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AnalyticsTracker" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"agentId" UUID NOT NULL,
|
||||
"views" INTEGER NOT NULL,
|
||||
"downloads" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Agents_id_key" ON "Agents"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AnalyticsTracker_id_key" ON "AnalyticsTracker"("id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
|
||||
-- Add trigger to update the search column with the tsvector of the agent
|
||||
-- Function to be invoked by trigger
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_tsvector_column() RETURNS TRIGGER AS $$
|
||||
|
||||
BEGIN
|
||||
|
||||
NEW.search := to_tsvector('english', COALESCE(NEW.description, '')|| ' ' ||COALESCE(NEW.name, '')|| ' ' ||COALESCE(NEW.author, ''));
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
END;
|
||||
|
||||
$$ LANGUAGE plpgsql SECURITY definer SET search_path = public, pg_temp;
|
||||
|
||||
-- Trigger that keeps the TSVECTOR up to date
|
||||
|
||||
DROP TRIGGER IF EXISTS "update_tsvector" ON "Agents";
|
||||
|
||||
CREATE TRIGGER "update_tsvector"
|
||||
|
||||
BEFORE INSERT OR UPDATE ON "Agents"
|
||||
|
||||
FOR EACH ROW
|
||||
|
||||
EXECUTE FUNCTION update_tsvector_column ();
|
@ -1,11 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[agentId]` on the table `AnalyticsTracker` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_pkey" PRIMARY KEY ("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AnalyticsTracker_agentId_key" ON "AnalyticsTracker"("agentId");
|
@ -1,20 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "FeaturedAgent" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"agentId" UUID NOT NULL,
|
||||
"is_featured" BOOLEAN NOT NULL,
|
||||
"category" TEXT NOT NULL DEFAULT 'featured',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "FeaturedAgent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "FeaturedAgent_id_key" ON "FeaturedAgent"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "FeaturedAgent_agentId_key" ON "FeaturedAgent"("agentId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FeaturedAgent" ADD CONSTRAINT "FeaturedAgent_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "FeaturedAgent" ALTER COLUMN "is_featured" SET DEFAULT false;
|
@ -1,8 +0,0 @@
|
||||
-- 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';
|
@ -1,12 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `category` on the `FeaturedAgent` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `is_featured` on the `FeaturedAgent` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "FeaturedAgent" DROP COLUMN "category",
|
||||
DROP COLUMN "is_featured",
|
||||
ADD COLUMN "featuredCategories" TEXT[],
|
||||
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT false;
|
@ -1,19 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "InstallationLocation" AS ENUM ('LOCAL', 'CLOUD');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InstallTracker" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"marketplaceAgentId" UUID NOT NULL,
|
||||
"installedAgentId" UUID NOT NULL,
|
||||
"installationLocation" "InstallationLocation" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "InstallTracker_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InstallTracker_marketplaceAgentId_installedAgentId_key" ON "InstallTracker"("marketplaceAgentId", "installedAgentId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InstallTracker" ADD CONSTRAINT "InstallTracker_marketplaceAgentId_fkey" FOREIGN KEY ("marketplaceAgentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -1,20 +0,0 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "AnalyticsTracker" DROP CONSTRAINT "AnalyticsTracker_agentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "FeaturedAgent" DROP CONSTRAINT "FeaturedAgent_agentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InstallTracker" DROP CONSTRAINT "InstallTracker_marketplaceAgentId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "AnalyticsTracker_agentId_key";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InstallTracker" ADD CONSTRAINT "InstallTracker_marketplaceAgentId_fkey" FOREIGN KEY ("marketplaceAgentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FeaturedAgent" ADD CONSTRAINT "FeaturedAgent_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -1,8 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[agentId]` on the table `AnalyticsTracker` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AnalyticsTracker_agentId_key" ON "AnalyticsTracker"("agentId");
|
@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
1301
autogpt_platform/market/poetry.lock
generated
1301
autogpt_platform/market/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,57 +0,0 @@
|
||||
[tool.poetry]
|
||||
name = "market"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
"SwiftyOS <craigswift13@gmail.com>",
|
||||
"Nicholas Tindle <spam@ntindle.com>",
|
||||
]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
prisma = "^0.15.0"
|
||||
python-dotenv = "^1.0.1"
|
||||
uvicorn = "^0.34.0"
|
||||
fastapi = "^0.115.6"
|
||||
sentry-sdk = { extras = ["fastapi"], version = "^2.19.2" }
|
||||
fuzzywuzzy = "^0.18.0"
|
||||
python-levenshtein = "^0.26.1"
|
||||
# autogpt-platform-backend = { path = "../backend", develop = true }
|
||||
prometheus-fastapi-instrumentator = "^7.0.0"
|
||||
|
||||
|
||||
autogpt-libs = {path = "../autogpt_libs"}
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.3.4"
|
||||
pytest-asyncio = "^0.25.0"
|
||||
|
||||
pytest-watcher = "^0.4.3"
|
||||
requests = "^2.32.3"
|
||||
ruff = "^0.8.3"
|
||||
pyright = "^1.1.390"
|
||||
isort = "^5.13.2"
|
||||
black = "^24.10.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
format = "scripts:format"
|
||||
lint = "scripts:lint"
|
||||
app = "scripts:app"
|
||||
setup = "scripts:setup"
|
||||
populate = "scripts:populate_database"
|
||||
|
||||
[tool.pytest-watcher]
|
||||
now = false
|
||||
clear = true
|
||||
delay = 0.2
|
||||
runner = "pytest"
|
||||
runner_args = []
|
||||
patterns = ["*.py"]
|
||||
ignore_patterns = []
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
@ -1,80 +0,0 @@
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-py"
|
||||
recursive_type_depth = 5
|
||||
interface = "asyncio"
|
||||
previewFeatures = ["fullTextSearch"]
|
||||
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
|
||||
version Int @default(1)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
submissionDate DateTime @default(now())
|
||||
submissionReviewDate DateTime?
|
||||
submissionStatus SubmissionStatus @default(PENDING)
|
||||
submissionReviewComments String?
|
||||
|
||||
name String?
|
||||
description String?
|
||||
author String?
|
||||
|
||||
keywords String[]
|
||||
categories String[]
|
||||
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
|
||||
|
||||
graph Json
|
||||
AnalyticsTracker AnalyticsTracker[]
|
||||
FeaturedAgent FeaturedAgent?
|
||||
InstallTracker InstallTracker[]
|
||||
|
||||
@@id(name: "graphVersionId", [id, version])
|
||||
}
|
||||
|
||||
model AnalyticsTracker {
|
||||
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
agentId String @unique @db.Uuid
|
||||
agent Agents @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
||||
views Int
|
||||
downloads Int
|
||||
}
|
||||
|
||||
enum InstallationLocation {
|
||||
LOCAL
|
||||
CLOUD
|
||||
}
|
||||
|
||||
model InstallTracker {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
marketplaceAgentId String @db.Uuid
|
||||
marketplaceAgent Agents @relation(fields: [marketplaceAgentId], references: [id], onDelete: Cascade)
|
||||
installedAgentId String @db.Uuid
|
||||
installationLocation InstallationLocation
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([marketplaceAgentId, installedAgentId])
|
||||
}
|
||||
|
||||
model FeaturedAgent {
|
||||
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
agentId String @unique @db.Uuid
|
||||
agent Agents @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
||||
isActive Boolean @default(false)
|
||||
featuredCategories String[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
directory = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def run(*command: str) -> None:
|
||||
print(f">>>>> Running poetry run {' '.join(command)}")
|
||||
subprocess.run(["poetry", "run"] + list(command), cwd=directory, check=True)
|
||||
|
||||
|
||||
def lint():
|
||||
try:
|
||||
run("ruff", "check", ".", "--exit-zero")
|
||||
run("isort", "--diff", "--check", "--profile", "black", ".")
|
||||
run("black", "--diff", "--check", ".")
|
||||
run("pyright")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("Lint failed, try running `poetry run format` to fix the issues: ", e)
|
||||
raise e
|
||||
|
||||
|
||||
def populate_database():
|
||||
import glob
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
import requests
|
||||
|
||||
import market.model
|
||||
|
||||
templates = pathlib.Path(__file__).parent.parent / "graph_templates"
|
||||
|
||||
all_files = glob.glob(str(templates / "*.json"))
|
||||
|
||||
for file in all_files:
|
||||
with open(file, "r") as f:
|
||||
data = f.read()
|
||||
req = market.model.AddAgentRequest(
|
||||
graph=json.loads(data),
|
||||
author="Populate DB",
|
||||
categories=["Pre-Populated"],
|
||||
keywords=["test"],
|
||||
)
|
||||
response = requests.post(
|
||||
"http://localhost:8015/api/v1/market/admin/agent", json=req.model_dump()
|
||||
)
|
||||
print(response.text)
|
||||
|
||||
|
||||
def format():
|
||||
run("ruff", "check", "--fix", ".")
|
||||
run("isort", "--profile", "black", ".")
|
||||
run("black", ".")
|
||||
run("pyright", ".")
|
||||
|
||||
|
||||
def app():
|
||||
port = os.getenv("PORT", "8015")
|
||||
run("uvicorn", "market.app:app", "--reload", "--port", port, "--host", "0.0.0.0")
|
||||
|
||||
|
||||
def setup():
|
||||
run("prisma", "generate")
|
||||
run("prisma", "migrate", "deploy")
|
@ -1,79 +0,0 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from market.app import app
|
||||
from market.db import AgentQueryError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client():
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
# Mock data
|
||||
mock_agents = [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Agent 1",
|
||||
"description": "Description 1",
|
||||
"author": "Author 1",
|
||||
"keywords": ["AI", "chatbot"],
|
||||
"categories": ["general"],
|
||||
"version": 1,
|
||||
"createdAt": datetime.now(timezone.utc),
|
||||
"updatedAt": datetime.now(timezone.utc),
|
||||
"graph": {"node1": "value1"},
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Agent 2",
|
||||
"description": "Description 2",
|
||||
"author": "Author 2",
|
||||
"keywords": ["ML", "NLP"],
|
||||
"categories": ["specialized"],
|
||||
"version": 1,
|
||||
"createdAt": datetime.now(timezone.utc),
|
||||
"updatedAt": datetime.now(timezone.utc),
|
||||
"graph": {"node2": "value2"},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# TODO: Need to mock prisma somehow
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_agents(test_client):
|
||||
response = test_client.get("/agents")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["agents"]) == 2
|
||||
assert data["total_count"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_agents_with_filters(test_client):
|
||||
response = await test_client.get("/agents?name=Agent 1&keyword=AI&category=general")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["agents"]) == 1
|
||||
assert data["agents"][0]["name"] == "Agent 1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_agent_details(test_client, mock_get_agent_details):
|
||||
response = await test_client.get("/agents/1")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == "1"
|
||||
assert data["name"] == "Agent 1"
|
||||
assert "graph" in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_agent(test_client, mock_get_agent_details):
|
||||
mock_get_agent_details.side_effect = AgentQueryError("Agent not found")
|
||||
response = await test_client.get("/agents/999")
|
||||
assert response.status_code == 404
|
Loading…
Reference in New Issue
Block a user