This commit is contained in:
SwiftyOS
2024-11-11 11:29:01 +01:00
parent 2068073e8c
commit f539c24571
22 changed files with 527 additions and 565 deletions

View File

@@ -11,6 +11,7 @@ import backend.data.block
import backend.data.db
import backend.data.user
import backend.server.routers.v1
import backend.server.v2.store.routes
import backend.util.service
import backend.util.settings
@@ -62,7 +63,10 @@ app = fastapi.FastAPI(
app.add_exception_handler(ValueError, handle_internal_http_error(400))
app.add_exception_handler(500, handle_internal_http_error(500))
app.include_router(backend.server.routers.v1.v1_router, tags=["v1"])
app.include_router(backend.server.routers.v1.v1_router, tags=["v1"], prefix="/api")
app.include_router(
backend.server.v2.store.routes.router, tags=["v2"], prefix="/api/store"
)
@app.get(path="/health", tags=["health"], dependencies=[])

View File

@@ -40,8 +40,7 @@ logger = logging.getLogger(__name__)
_user_credit_model = get_user_credit_model()
# Define the API routes
v1_router = APIRouter(prefix="/api")
v1_router = APIRouter()
v1_router.dependencies.append(Depends(auth_middleware))

View File

@@ -1,12 +1,13 @@
import logging
import backend.server.v2.store.exceptions
import backend.server.v2.store.model
import prisma.enums
import prisma.errors
import prisma.models
import prisma.types
import backend.server.v2.store.exceptions
import backend.server.v2.store.model
logger = logging.getLogger(__name__)
@@ -84,7 +85,9 @@ async def get_store_agents(
)
except Exception as e:
logger.error(f"Error getting store agents: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError("Failed to fetch store agents") from e
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch store agents"
) from e
async def get_store_agent_details(
@@ -99,7 +102,9 @@ async def get_store_agent_details(
if not agent:
logger.warning(f"Agent not found: {username}/{agent_name}")
raise backend.server.v2.store.exceptions.AgentNotFoundError(f"Agent {username}/{agent_name} not found")
raise backend.server.v2.store.exceptions.AgentNotFoundError(
f"Agent {username}/{agent_name} not found"
)
logger.debug(f"Found agent details for {username}/{agent_name}")
return backend.server.v2.store.model.StoreAgentDetails(
@@ -120,7 +125,9 @@ async def get_store_agent_details(
raise
except Exception as e:
logger.error(f"Error getting store agent details: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError("Failed to fetch agent details") from e
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch agent details"
) from e
async def get_store_creators(
@@ -199,7 +206,9 @@ async def get_store_creators(
)
except Exception as e:
logger.error(f"Error getting store creators: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError("Failed to fetch store creators") from e
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch store creators"
) from e
async def get_store_creator_details(
@@ -215,7 +224,9 @@ async def get_store_creator_details(
if not creator:
logger.warning(f"Creator not found: {username}")
raise backend.server.v2.store.exceptions.CreatorNotFoundError(f"Creator {username} not found")
raise backend.server.v2.store.exceptions.CreatorNotFoundError(
f"Creator {username} not found"
)
logger.debug(f"Found creator details for {username}")
return backend.server.v2.store.model.CreatorDetails(
@@ -232,7 +243,9 @@ async def get_store_creator_details(
raise
except Exception as e:
logger.error(f"Error getting store creator details: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError("Failed to fetch creator details") from e
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch creator details"
) from e
async def get_store_submissions(
@@ -339,7 +352,9 @@ async def create_store_submission(
logger.warning(
f"Agent not found for user {user_id}: {agent_id} v{agent_version}"
)
raise backend.server.v2.store.exceptions.AgentNotFoundError("Agent not found for this user")
raise backend.server.v2.store.exceptions.AgentNotFoundError(
"Agent not found for this user"
)
listing = await prisma.models.StoreListing.prisma().find_first(
where=prisma.types.StoreListingWhereInput(
@@ -348,7 +363,9 @@ async def create_store_submission(
)
if listing is not None:
logger.warning(f"Listing already exists for agent {agent_id}")
raise backend.server.v2.store.exceptions.ListingExistsError("Listing already exists for this agent")
raise backend.server.v2.store.exceptions.ListingExistsError(
"Listing already exists for this agent"
)
# Create the store listing
listing = await prisma.models.StoreListing.prisma().create(
@@ -384,11 +401,16 @@ async def create_store_submission(
rating=0.0,
)
except (backend.server.v2.store.exceptions.AgentNotFoundError, backend.server.v2.store.exceptions.ListingExistsError):
except (
backend.server.v2.store.exceptions.AgentNotFoundError,
backend.server.v2.store.exceptions.ListingExistsError,
):
raise
except prisma.errors.PrismaError as e:
logger.error(f"Database error creating store submission: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError("Failed to create store submission") from e
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create store submission"
) from e
async def update_profile(
@@ -455,7 +477,9 @@ async def update_profile(
)
if updated_profile is None:
logger.error(f"Failed to update profile for user {user_id}")
raise backend.server.v2.store.exceptions.DatabaseError("Failed to update profile")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update profile"
)
return backend.server.v2.store.model.CreatorDetails(
name=updated_profile.name,
@@ -470,4 +494,6 @@ async def update_profile(
except prisma.errors.PrismaError as e:
logger.error(f"Database error updating profile: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError("Failed to update profile") from e
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update profile"
) from e

View File

@@ -1,18 +1,20 @@
from datetime import datetime
import backend.server.v2.store.db as db
import prisma.errors
import prisma.models
import pytest
from backend.server.v2.store.model import CreatorDetails
from prisma import Prisma, register
import backend.server.v2.store.db as db
import backend.server.v2.store.exceptions
from backend.server.v2.store.model import CreatorDetails
@pytest.fixture(autouse=True)
async def setup_prisma():
try:
register(Prisma())
except:
except backend.server.v2.store.exceptions.DatabaseError:
pass
yield

View File

@@ -33,26 +33,32 @@ class StorageUploadError(MediaUploadError):
pass
class StoreError(Exception):
"""Base exception for store-related errors"""
pass
class AgentNotFoundError(StoreError):
"""Raised when an agent is not found"""
pass
class CreatorNotFoundError(StoreError):
"""Raised when a creator is not found"""
pass
class ListingExistsError(StoreError):
"""Raised when trying to create a listing that already exists"""
pass
class DatabaseError(StoreError):
"""Raised when there is an error interacting with the database"""
pass
pass

View File

@@ -2,10 +2,11 @@ import logging
import os
import uuid
import backend.server.v2.store.exceptions
import fastapi
import supabase
import backend.server.v2.store.exceptions
logger = logging.getLogger(__name__)
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
@@ -19,7 +20,9 @@ async def upload_media(user_id: str, file: fastapi.UploadFile) -> str:
supabase_key = os.environ["SUPABASE_KEY"]
except KeyError as e:
logger.error(f"Missing required environment variable: {str(e)}")
raise backend.server.v2.store.exceptions.StorageConfigError("Missing storage configuration") from e
raise backend.server.v2.store.exceptions.StorageConfigError(
"Missing storage configuration"
) from e
try:
# Validate file type
@@ -42,12 +45,16 @@ async def upload_media(user_id: str, file: fastapi.UploadFile) -> str:
file_size += len(chunk)
if file_size > MAX_FILE_SIZE:
logger.warning(f"File size too large: {file_size} bytes")
raise backend.server.v2.store.exceptions.FileSizeTooLargeError("File too large. Maximum size is 50MB")
raise backend.server.v2.store.exceptions.FileSizeTooLargeError(
"File too large. Maximum size is 50MB"
)
except backend.server.v2.store.exceptions.FileSizeTooLargeError:
raise
except Exception as e:
logger.error(f"Error reading file chunks: {str(e)}")
raise backend.server.v2.store.exceptions.FileReadError("Failed to read uploaded file") from e
raise backend.server.v2.store.exceptions.FileReadError(
"Failed to read uploaded file"
) from e
# Reset file pointer
await file.seek(0)
@@ -80,10 +87,14 @@ async def upload_media(user_id: str, file: fastapi.UploadFile) -> str:
except Exception as e:
logger.error(f"Supabase storage error: {str(e)}")
raise backend.server.v2.store.exceptions.StorageUploadError("Failed to upload file to storage") from e
raise backend.server.v2.store.exceptions.StorageUploadError(
"Failed to upload file to storage"
) from e
except backend.server.v2.store.exceptions.MediaUploadError:
raise
except Exception as e:
logger.exception("Unexpected error in upload_media")
raise backend.server.v2.store.exceptions.MediaUploadError("Unexpected error during media upload") from e
raise backend.server.v2.store.exceptions.MediaUploadError(
"Unexpected error during media upload"
) from e

View File

@@ -1,12 +1,13 @@
import io
import unittest.mock
import backend.server.v2.store.exceptions
import backend.server.v2.store.media
import fastapi
import pytest
import starlette.datastructures
import backend.server.v2.store.exceptions
import backend.server.v2.store.media
@pytest.fixture
def mock_env_vars(monkeypatch):

View File

@@ -271,7 +271,6 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
# Add more secret fields as needed
model_config = SettingsConfigDict(
secrets_dir=get_secrets_path(),
env_file=".env",
env_file_encoding="utf-8",
extra="allow",

View File

@@ -1,9 +1,10 @@
import asyncio
import random
from datetime import datetime, timedelta
from prisma import Prisma
from faker import Faker
from datetime import datetime
import prisma.enums
from faker import Faker
from prisma import Prisma
faker = Faker()
@@ -19,7 +20,9 @@ MAX_GRAPHS_PER_USER = 5 # Total graphs: 500-2500 (NUM_USERS * MIN/MAX_GRAPHS)
# Per-graph entities
MIN_NODES_PER_GRAPH = 2 # Each graph will have between 2-5 nodes
MAX_NODES_PER_GRAPH = 5 # Total nodes: 1000-2500 (GRAPHS_PER_USER * NUM_USERS * MIN/MAX_NODES)
MAX_NODES_PER_GRAPH = (
5 # Total nodes: 1000-2500 (GRAPHS_PER_USER * NUM_USERS * MIN/MAX_NODES)
)
# Additional per-user entities
MIN_PRESETS_PER_USER = 1 # Each user will have between 1-2 presets
@@ -29,7 +32,9 @@ MAX_AGENTS_PER_USER = 10 # Total agents: 500-5000 (NUM_USERS * MIN/MAX_AGENTS)
# Execution and review records
MIN_EXECUTIONS_PER_GRAPH = 1 # Each graph will have between 1-5 execution records
MAX_EXECUTIONS_PER_GRAPH = 20 # Total executions: 1000-5000 (TOTAL_GRAPHS * MIN/MAX_EXECUTIONS)
MAX_EXECUTIONS_PER_GRAPH = (
20 # Total executions: 1000-5000 (TOTAL_GRAPHS * MIN/MAX_EXECUTIONS)
)
MIN_REVIEWS_PER_VERSION = 1 # Each version will have between 1-3 reviews
MAX_REVIEWS_PER_VERSION = 5 # Total reviews depends on number of versions created
@@ -44,11 +49,11 @@ async def main():
for _ in range(NUM_USERS):
user = await db.user.create(
data={
'id': str(faker.uuid4()),
'email': faker.unique.email(),
'name': faker.name(),
'metadata': prisma.Json({}),
'integrations': '',
"id": str(faker.uuid4()),
"email": faker.unique.email(),
"name": faker.name(),
"metadata": prisma.Json({}),
"integrations": "",
}
)
users.append(user)
@@ -59,9 +64,9 @@ async def main():
for _ in range(NUM_AGENT_BLOCKS):
block = await db.agentblock.create(
data={
'name': f"{faker.word()}_{str(faker.uuid4())[:8]}",
'inputSchema': '{}',
'outputSchema': '{}',
"name": f"{faker.word()}_{str(faker.uuid4())[:8]}",
"inputSchema": "{}",
"outputSchema": "{}",
}
)
agent_blocks.append(block)
@@ -70,32 +75,36 @@ async def main():
agent_graphs = []
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent graphs")
for user in users:
for _ in range(random.randint(MIN_GRAPHS_PER_USER, MAX_GRAPHS_PER_USER)): # Adjust the range to create more graphs per user if desired
for _ in range(
random.randint(MIN_GRAPHS_PER_USER, MAX_GRAPHS_PER_USER)
): # Adjust the range to create more graphs per user if desired
graph = await db.agentgraph.create(
data={
'name': faker.sentence(nb_words=3),
'description': faker.text(max_nb_chars=200),
'userId': user.id,
'isActive': True,
'isTemplate': False,
"name": faker.sentence(nb_words=3),
"description": faker.text(max_nb_chars=200),
"userId": user.id,
"isActive": True,
"isTemplate": False,
}
)
agent_graphs.append(graph)
# Insert AgentNodes
agent_nodes = []
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_NODES_PER_GRAPH} agent nodes")
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_NODES_PER_GRAPH} agent nodes"
)
for graph in agent_graphs:
num_nodes = random.randint(MIN_NODES_PER_GRAPH, MAX_NODES_PER_GRAPH)
for _ in range(num_nodes): # Create 5 AgentNodes per graph
block = random.choice(agent_blocks)
node = await db.agentnode.create(
data={
'agentBlockId': block.id,
'agentGraphId': graph.id,
'agentGraphVersion': graph.version,
'constantInput': '{}',
'metadata': '{}',
"agentBlockId": block.id,
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
"constantInput": "{}",
"metadata": "{}",
}
)
agent_nodes.append(node)
@@ -109,12 +118,12 @@ async def main():
graph = random.choice(agent_graphs)
preset = await db.agentpreset.create(
data={
'name': faker.sentence(nb_words=3),
'description': faker.text(max_nb_chars=200),
'userId': user.id,
'agentId': graph.id,
'agentVersion': graph.version,
'isActive': True,
"name": faker.sentence(nb_words=3),
"description": faker.text(max_nb_chars=200),
"userId": user.id,
"agentId": graph.id,
"agentVersion": graph.version,
"isActive": True,
}
)
agent_presets.append(preset)
@@ -129,14 +138,14 @@ async def main():
preset = random.choice(agent_presets)
user_agent = await db.useragent.create(
data={
'userId': user.id,
'agentId': graph.id,
'agentVersion': graph.version,
'agentPresetId': preset.id,
'isFavorite': random.choice([True, False]),
'isCreatedByUser': random.choice([True, False]),
'isArchived': random.choice([True, False]),
'isDeleted': random.choice([True, False]),
"userId": user.id,
"agentId": graph.id,
"agentVersion": graph.version,
"agentPresetId": preset.id,
"isFavorite": random.choice([True, False]),
"isCreatedByUser": random.choice([True, False]),
"isArchived": random.choice([True, False]),
"isDeleted": random.choice([True, False]),
}
)
user_agents.append(user_agent)
@@ -144,24 +153,34 @@ async def main():
# Insert AgentGraphExecutions
# Insert AgentGraphExecutions
agent_graph_executions = []
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent graph executions")
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent graph executions"
)
graph_execution_data = []
for graph in agent_graphs:
user = random.choice(users)
num_executions = random.randint(MIN_EXECUTIONS_PER_GRAPH, MAX_EXECUTIONS_PER_GRAPH)
num_executions = random.randint(
MIN_EXECUTIONS_PER_GRAPH, MAX_EXECUTIONS_PER_GRAPH
)
for _ in range(num_executions):
matching_presets = [p for p in agent_presets if p.agentId == graph.id]
preset = random.choice(matching_presets) if matching_presets and random.random() < 0.5 else None
graph_execution_data.append({
'agentGraphId': graph.id,
'agentGraphVersion': graph.version,
'userId': user.id,
'executionStatus': prisma.enums.AgentExecutionStatus.COMPLETED,
'startedAt': faker.date_time_this_year(),
'agentPresetId': preset.id if preset else None,
})
preset = (
random.choice(matching_presets)
if matching_presets and random.random() < 0.5
else None
)
graph_execution_data.append(
{
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
"userId": user.id,
"executionStatus": prisma.enums.AgentExecutionStatus.COMPLETED,
"startedAt": faker.date_time_this_year(),
"agentPresetId": preset.id if preset else None,
}
)
agent_graph_executions = await db.agentgraphexecution.create_many(
data=graph_execution_data
)
@@ -169,18 +188,24 @@ async def main():
agent_graph_executions = await db.agentgraphexecution.find_many()
# Insert AgentNodeExecutions
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent node executions")
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent node executions"
)
node_execution_data = []
for execution in agent_graph_executions:
nodes = [node for node in agent_nodes if node.agentGraphId == execution.agentGraphId]
nodes = [
node for node in agent_nodes if node.agentGraphId == execution.agentGraphId
]
for node in nodes:
node_execution_data.append({
'agentGraphExecutionId': execution.id,
'agentNodeId': node.id,
'executionStatus': prisma.enums.AgentExecutionStatus.COMPLETED,
'addedTime': datetime.now(),
})
node_execution_data.append(
{
"agentGraphExecutionId": execution.id,
"agentNodeId": node.id,
"executionStatus": prisma.enums.AgentExecutionStatus.COMPLETED,
"addedTime": datetime.now(),
}
)
agent_node_executions = await db.agentnodeexecution.create_many(
data=node_execution_data
)
@@ -188,43 +213,49 @@ async def main():
agent_node_executions = await db.agentnodeexecution.find_many()
# Insert AgentNodeExecutionInputOutput
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent node execution input/outputs")
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent node execution input/outputs"
)
input_output_data = []
for node_execution in agent_node_executions:
# Input data
input_output_data.append({
'name': 'input1',
'data': '{}',
'time': datetime.now(),
'referencedByInputExecId': node_execution.id,
})
input_output_data.append(
{
"name": "input1",
"data": "{}",
"time": datetime.now(),
"referencedByInputExecId": node_execution.id,
}
)
# Output data
input_output_data.append({
'name': 'output1',
'data': '{}',
'time': datetime.now(),
'referencedByOutputExecId': node_execution.id,
})
input_output_data.append(
{
"name": "output1",
"data": "{}",
"time": datetime.now(),
"referencedByOutputExecId": node_execution.id,
}
)
await db.agentnodeexecutioninputoutput.create_many(
data=input_output_data
)
await db.agentnodeexecutioninputoutput.create_many(data=input_output_data)
# Insert AgentGraphExecutionSchedules
agent_graph_execution_schedules = []
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent graph execution schedules")
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent graph execution schedules"
)
for graph in agent_graphs:
user = random.choice(users)
schedule = await db.agentgraphexecutionschedule.create(
data={
'id': str(faker.uuid4()),
'agentGraphId': graph.id,
'agentGraphVersion': graph.version,
'schedule': '* * * * *',
'isEnabled': True,
'inputData': '{}',
'userId': user.id,
'lastUpdated': datetime.now(),
"id": str(faker.uuid4()),
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
"schedule": "* * * * *",
"isEnabled": True,
"inputData": "{}",
"userId": user.id,
"lastUpdated": datetime.now(),
}
)
agent_graph_execution_schedules.append(schedule)
@@ -236,13 +267,13 @@ async def main():
if len(nodes) >= 2:
source_node = nodes[0]
sink_node = nodes[1]
link = await db.agentnodelink.create(
await db.agentnodelink.create(
data={
'agentNodeSourceId': source_node.id,
'sourceName': 'output1',
'agentNodeSinkId': sink_node.id,
'sinkName': 'input1',
'isStatic': False,
"agentNodeSourceId": source_node.id,
"sourceName": "output1",
"agentNodeSinkId": sink_node.id,
"sinkName": "input1",
"isStatic": False,
}
)
@@ -250,12 +281,12 @@ async def main():
print(f"Inserting {NUM_USERS} analytics details")
for user in users:
for _ in range(1):
analytic_detail = await db.analyticsdetails.create(
await db.analyticsdetails.create(
data={
'userId': user.id,
'type': faker.word(),
'data': prisma.Json({}),
'dataIndex': faker.word(),
"userId": user.id,
"type": faker.word(),
"data": prisma.Json({}),
"dataIndex": faker.word(),
}
)
@@ -263,12 +294,12 @@ async def main():
print(f"Inserting {NUM_USERS} analytics metrics")
for user in users:
for _ in range(1):
metric = await db.analyticsmetrics.create(
await db.analyticsmetrics.create(
data={
'userId': user.id,
'analyticMetric': faker.word(),
'value': random.uniform(0, 100),
'dataString': faker.word(),
"userId": user.id,
"analyticMetric": faker.word(),
"value": random.uniform(0, 100),
"dataString": faker.word(),
}
)
@@ -277,14 +308,18 @@ async def main():
for user in users:
for _ in range(1):
block = random.choice(agent_blocks)
credit = await db.userblockcredit.create(
await db.userblockcredit.create(
data={
'transactionKey': str(faker.uuid4()),
'userId': user.id,
'blockId': block.id,
'amount': random.randint(1, 100),
'type': prisma.enums.UserBlockCreditType.TOP_UP if random.random() < 0.5 else prisma.enums.UserBlockCreditType.USAGE,
'metadata': prisma.Json({}),
"transactionKey": str(faker.uuid4()),
"userId": user.id,
"blockId": block.id,
"amount": random.randint(1, 100),
"type": (
prisma.enums.UserBlockCreditType.TOP_UP
if random.random() < 0.5
else prisma.enums.UserBlockCreditType.USAGE
),
"metadata": prisma.Json({}),
}
)
@@ -294,12 +329,12 @@ async def main():
for user in users:
profile = await db.profile.create(
data={
'userId': user.id,
'name': user.name or faker.name(),
'username': faker.unique.user_name(),
'description': faker.text(),
'links': [faker.url() for _ in range(3)],
'avatarUrl': faker.image_url(),
"userId": user.id,
"name": user.name or faker.name(),
"username": faker.unique.user_name(),
"description": faker.text(),
"links": [faker.url() for _ in range(3)],
"avatarUrl": faker.image_url(),
}
)
profiles.append(profile)
@@ -311,10 +346,10 @@ async def main():
user = random.choice(users)
listing = await db.storelisting.create(
data={
'agentId': graph.id,
'agentVersion': graph.version,
'owningUserId': user.id,
'isApproved': random.choice([True, False]),
"agentId": graph.id,
"agentVersion": graph.version,
"owningUserId": user.id,
"isApproved": random.choice([True, False]),
}
)
store_listings.append(listing)
@@ -326,19 +361,19 @@ async def main():
graph = [g for g in agent_graphs if g.id == listing.agentId][0]
version = await db.storelistingversion.create(
data={
'agentId': graph.id,
'agentVersion': graph.version,
'slug': faker.slug(),
'name': graph.name or faker.sentence(nb_words=3),
'subHeading': faker.sentence(),
'videoUrl': faker.url(),
'imageUrls': [faker.image_url() for _ in range(3)],
'description': faker.text(),
'categories': [faker.word() for _ in range(3)],
'isFeatured': random.choice([True, False]),
'isAvailable': True,
'isApproved': random.choice([True, False]),
'storeListingId': listing.id,
"agentId": graph.id,
"agentVersion": graph.version,
"slug": faker.slug(),
"name": graph.name or faker.sentence(nb_words=3),
"subHeading": faker.sentence(),
"videoUrl": faker.url(),
"imageUrls": [faker.image_url() for _ in range(3)],
"description": faker.text(),
"categories": [faker.word() for _ in range(3)],
"isFeatured": random.choice([True, False]),
"isAvailable": True,
"isApproved": random.choice([True, False]),
"storeListingId": listing.id,
}
)
store_listing_versions.append(version)
@@ -349,12 +384,12 @@ async def main():
num_reviews = random.randint(MIN_REVIEWS_PER_VERSION, MAX_REVIEWS_PER_VERSION)
for _ in range(num_reviews):
user = random.choice(users)
review = await db.storelistingreview.create(
await db.storelistingreview.create(
data={
'storeListingVersionId': version.id,
'reviewByUserId': user.id,
'score': random.randint(1, 5),
'comments': faker.text(),
"storeListingVersionId": version.id,
"reviewByUserId": user.id,
"score": random.randint(1, 5),
"comments": faker.text(),
}
)
@@ -363,37 +398,44 @@ async def main():
for listing in store_listings:
version = random.choice(store_listing_versions)
reviewer = random.choice(users)
status: prisma.enums.SubmissionStatus = random.choice([prisma.enums.SubmissionStatus.PENDING, prisma.enums.SubmissionStatus.APPROVED, prisma.enums.SubmissionStatus.REJECTED])
submission = await db.storelistingsubmission.create(
status: prisma.enums.SubmissionStatus = random.choice(
[
prisma.enums.SubmissionStatus.PENDING,
prisma.enums.SubmissionStatus.APPROVED,
prisma.enums.SubmissionStatus.REJECTED,
]
)
await db.storelistingsubmission.create(
data={
'storeListingId': listing.id,
'storeListingVersionId': version.id,
'reviewerId': reviewer.id,
'Status': status,
'reviewComments': faker.text(),
"storeListingId": listing.id,
"storeListingVersionId": version.id,
"reviewerId": reviewer.id,
"Status": status,
"reviewComments": faker.text(),
}
)
# Insert APIKeys
print(f"Inserting {NUM_USERS} api keys")
for user in users:
api_key = await db.apikey.create(
await db.apikey.create(
data={
'name': faker.word(),
'prefix': str(faker.uuid4())[:8],
'postfix': str(faker.uuid4())[-8:],
'key': str(faker.sha256()),
'status': prisma.enums.APIKeyStatus.ACTIVE,
'permissions': [
"name": faker.word(),
"prefix": str(faker.uuid4())[:8],
"postfix": str(faker.uuid4())[-8:],
"key": str(faker.sha256()),
"status": prisma.enums.APIKeyStatus.ACTIVE,
"permissions": [
prisma.enums.APIKeyPermission.EXECUTE_GRAPH,
prisma.enums.APIKeyPermission.READ_GRAPH
prisma.enums.APIKeyPermission.READ_GRAPH,
],
'description': faker.text(),
'userId': user.id,
"description": faker.text(),
"userId": user.id,
}
)
await db.disconnect()
if __name__ == '__main__':
asyncio.run(main())
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -38,3 +38,38 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
redirect("/");
});
}
export async function signup(values: z.infer<typeof loginFormSchema>) {
"use server";
return await Sentry.withServerActionInstrumentation(
"signup",
{},
async () => {
const supabase = createServerClient();
if (!supabase) {
redirect("/error");
}
// We are sure that the values are of the correct type because zod validates the form
const { data, error } = await supabase.auth.signUp(values);
if (error) {
if (error.message.includes("P0001")) {
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
}
if (error.code?.includes("user_already_exists")) {
redirect("/login");
}
return error.message;
}
if (data.session) {
await supabase.auth.setSession(data.session);
}
revalidatePath("/", "layout");
redirect("/");
},
);
}

View File

@@ -203,14 +203,34 @@ export default function LoginPage() {
className="flex w-full justify-center"
type="submit"
disabled={isLoading}
onClick={async () => {
setIsLoading(true);
const values = form.getValues();
const result = await login(values);
if (result) {
setFeedback(result);
}
setIsLoading(false);
}}
>
Log in
{isLoading ? <FaSpinner className="animate-spin" /> : "Log in"}
</Button>
<Button
className="flex w-full justify-center"
type="button"
disabled={isLoading}
onClick={async () => {
setIsLoading(true);
const values = form.getValues();
const result = await signup(values);
if (result) {
setFeedback(result);
}
setIsLoading(false);
}}
>
{isLoading ? <FaSpinner className="animate-spin" /> : "Sign up"}
</Button>
</div>
<div className="w-full text-center">
<Link href={"/signup"} className="w-fit text-xs hover:underline">
Create a new Account
</Link>
</div>
</form>
<p className="text-sm text-red-500">{feedback}</p>

View File

@@ -10,6 +10,8 @@ import TallyPopupSimple from "@/components/TallyPopup";
import { GoogleAnalytics } from "@next/third-parties/google";
import { Toaster } from "@/components/ui/toaster";
import { createServerClient } from "@/lib/supabase/server";
// Import Fonts
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
@@ -21,15 +23,22 @@ export const metadata: Metadata = {
description: "Your one stop shop to creating AI Agents",
};
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = createServerClient();
const {
data: { session },
} = await supabase.auth.getSession();
return (
<html lang="en">
<body className={cn("antialiased transition-colors", inter.className)}>
<Providers
initialSession={session}
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default

View File

@@ -4,19 +4,21 @@ import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes";
import { TooltipProvider } from "@/components/ui/tooltip";
import { AuthProvider } from "@/components/providers/AuthContext";
import SupabaseProvider from "@/components/providers/SupabaseProvider";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import { Session } from "@supabase/supabase-js";
export function Providers({ children, ...props }: ThemeProviderProps) {
export function Providers({
children,
initialSession,
...props
}: ThemeProviderProps & { initialSession: Session | null }) {
return (
<NextThemesProvider {...props}>
<SupabaseProvider>
<AuthProvider>
<CredentialsProvider>
<TooltipProvider>{children}</TooltipProvider>
</CredentialsProvider>
</AuthProvider>
<SupabaseProvider initialSession={initialSession}>
<CredentialsProvider>
<TooltipProvider>{children}</TooltipProvider>
</CredentialsProvider>
</SupabaseProvider>
</NextThemesProvider>
);

View File

@@ -1,46 +0,0 @@
"use server";
import { createServerClient } from "@/lib/supabase/server";
import * as Sentry from "@sentry/nextjs";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
const SignupFormSchema = z.object({
email: z.string().email().min(2).max(64),
password: z.string().min(6).max(64),
});
export async function signup(values: z.infer<typeof SignupFormSchema>) {
"use server";
return await Sentry.withServerActionInstrumentation(
"signup",
{},
async () => {
const supabase = createServerClient();
if (!supabase) {
redirect("/error");
}
// We are sure that the values are of the correct type because zod validates the form
const { data, error } = await supabase.auth.signUp(values);
if (error) {
if (error.message.includes("P0001")) {
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
}
if (error.code?.includes("user_already_exists")) {
redirect("/login");
}
return error.message;
}
if (data.session) {
await supabase.auth.setSession(data.session);
}
revalidatePath("/", "layout");
redirect("/");
},
);
}

View File

@@ -1,225 +0,0 @@
"use client";
import useUser from "@/hooks/useUser";
import { signup } from "./actions";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { PasswordInput } from "@/components/PasswordInput";
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
import { useState } from "react";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Checkbox } from "@/components/ui/checkbox";
const signupFormSchema = z.object({
email: z.string().email().min(2).max(64),
password: z.string().min(6).max(64),
agreeToTerms: z.boolean().refine((value) => value === true, {
message: "You must agree to the Terms of Use and Privacy Policy",
}),
});
export default function LoginPage() {
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
const { user, isLoading: isUserLoading } = useUser();
const [feedback, setFeedback] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const form = useForm<z.infer<typeof signupFormSchema>>({
resolver: zodResolver(signupFormSchema),
defaultValues: {
email: "",
password: "",
agreeToTerms: false,
},
});
if (user) {
console.log("User exists, redirecting to home");
router.push("/");
}
if (isUserLoading || isSupabaseLoading || user) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
}
if (!supabase) {
return (
<div>
User accounts are disabled because Supabase client is unavailable
</div>
);
}
async function handleSignInWithProvider(
provider: "google" | "github" | "discord",
) {
const { data, error } = await supabase!.auth.signInWithOAuth({
provider: provider,
options: {
redirectTo:
process.env.AUTH_CALLBACK_URL ??
`http://localhost:3000/auth/callback`,
},
});
if (!error) {
setFeedback(null);
return;
}
setFeedback(error.message);
}
const onSignup = async (data: z.infer<typeof signupFormSchema>) => {
if (await form.trigger()) {
setIsLoading(true);
const error = await signup(data);
setIsLoading(false);
if (error) {
setFeedback(error);
return;
}
setFeedback(null);
}
};
return (
<div className="flex h-[80vh] items-center justify-center">
<div className="w-full max-w-md space-y-6 rounded-lg p-8 shadow-md">
<h1 className="text-lg font-medium">Create a New Account</h1>
{/* <div className="mb-6 space-y-2">
<Button
className="w-full"
onClick={() => handleSignInWithProvider("google")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaGoogle className="mr-2 h-4 w-4" />
Sign in with Google
</Button>
<Button
className="w-full"
onClick={() => handleSignInWithProvider("github")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaGithub className="mr-2 h-4 w-4" />
Sign in with GitHub
</Button>
<Button
className="w-full"
onClick={() => handleSignInWithProvider("discord")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaDiscord className="mr-2 h-4 w-4" />
Sign in with Discord
</Button>
</div> */}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSignup)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="mb-4">
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="user@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput placeholder="password" {...field} />
</FormControl>
<FormDescription>
Password needs to be at least 6 characters long
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="agreeToTerms"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
I agree to the{" "}
<Link
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
className="underline"
>
Terms of Use
</Link>{" "}
and{" "}
<Link
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
className="underline"
>
Privacy Policy
</Link>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
<div className="mb-6 mt-8 flex w-full space-x-4">
<Button
className="flex w-full justify-center"
variant="outline"
type="button"
onClick={form.handleSubmit(onSignup)}
disabled={isLoading}
>
Sign up
</Button>
</div>
<div className="w-full text-center">
<Link href={"/login"} className="w-fit text-xs hover:underline">
Already a member? Log In here
</Link>
</div>
</form>
<p className="text-sm text-red-500">{feedback}</p>
</Form>
</div>
</div>
);
}

View File

@@ -1,3 +1,5 @@
"use client";
import * as React from "react";
import { FeaturedStoreCard } from "@/components/agptui/FeaturedStoreCard";
import {

View File

@@ -1,91 +0,0 @@
/**
* Authentication Context and Provider for Supabase Auth
*
* This module provides authentication state management using Supabase.
*
* Usage:
* 1. Wrap your app with AuthProvider:
* ```tsx
* <AuthProvider>
* <App />
* </AuthProvider>
* ```
*
* 2. Access auth state in child components using useAuth hook:
* ```tsx
* const MyComponent = () => {
* const { user, session, role } = useAuth();
*
* if (!user) return <div>Please log in</div>;
*
* return <div>Welcome {user.email}</div>;
* };
* ```
*
* The context provides:
* - user: Current authenticated User object or null
* - session: Current Session object or null
* - role: User's role string or null
*/
import { User, Session } from "@supabase/supabase-js";
import { createContext, useContext } from "react";
import { createServerClient } from "@/lib/supabase/server";
interface AuthContextType {
user: User | null;
session: Session | null;
role: string | null;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export async function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
const supabase = createServerClient();
if (!supabase) {
console.error("Could not create Supabase client");
return null;
}
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
const {
data: { session },
error: sessionError,
} = await supabase.auth.getSession();
if (userError) {
console.error("Error fetching user:", userError);
}
if (sessionError) {
console.error("Error fetching session:", sessionError);
}
return (
<AuthContext.Provider
value={{
user: user || null,
session: session || null,
role: user?.role || null,
}}
>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used inside AuthProvider");
}
return context;
};

View File

@@ -2,6 +2,7 @@
import { createClient } from "@/lib/supabase/client";
import { SupabaseClient } from "@supabase/supabase-js";
import { Session } from "@supabase/supabase-js";
import { useRouter } from "next/navigation";
import { createContext, useContext, useEffect, useState } from "react";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
@@ -15,9 +16,12 @@ const Context = createContext<SupabaseContextType | undefined>(undefined);
export default function SupabaseProvider({
children,
initialSession,
}: {
children: React.ReactNode;
initialSession: Session | null;
}) {
const [session, setSession] = useState<Session | null>(initialSession);
const [supabase, setSupabase] = useState<SupabaseClient | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
@@ -34,6 +38,7 @@ export default function SupabaseProvider({
const {
data: { subscription },
} = client.auth.onAuthStateChange((event, session) => {
setSession(session);
if (event === "SIGNED_IN") {
api.createUser();
}

View File

@@ -16,6 +16,13 @@ import {
NodeExecutionResult,
OAuth2Credentials,
User,
StoreAgentsResponse,
StoreAgentDetails,
CreatorsResponse,
CreatorDetails,
StoreSubmissionsResponse,
StoreSubmissionRequest,
StoreSubmission,
} from "./types";
export default class BaseAutoGPTServerAPI {
@@ -28,7 +35,7 @@ export default class BaseAutoGPTServerAPI {
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
"http://localhost:8006/api/v1",
"http://localhost:8006/api/",
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
"ws://localhost:8001/ws",
supabaseClient: SupabaseClient | null = null,
@@ -234,6 +241,70 @@ export default class BaseAutoGPTServerAPI {
return this._request("POST", "/analytics/log_raw_analytics", analytic);
}
///////////////////////////////////////////
/////////// V2 STORE API /////////////////
/////////////////////////////////////////
getStoreAgents(params?: {
featured?: boolean;
creator?: string;
sorted_by?: string;
search_query?: string;
category?: string;
page?: number;
page_size?: number;
}): Promise<StoreAgentsResponse> {
return this._get("/store/agents", params);
}
getStoreAgent(
username: string,
agentName: string,
): Promise<StoreAgentDetails> {
return this._get(`/store/agents/${username}/${agentName}`);
}
getStoreCreators(params?: {
featured?: boolean;
search_query?: string;
sorted_by?: string;
page?: number;
page_size?: number;
}): Promise<CreatorsResponse> {
return this._get("/store/creators", params);
}
getStoreCreator(username: string): Promise<CreatorDetails> {
return this._get(`/store/creator/${username}`);
}
getStoreSubmissions(params?: {
page?: number;
page_size?: number;
}): Promise<StoreSubmissionsResponse> {
return this._get("/store/submissions", params);
}
createStoreSubmission(
submission: StoreSubmissionRequest,
): Promise<StoreSubmission> {
return this._request("POST", "/store/submissions", submission);
}
uploadStoreSubmissionMedia(file: File): Promise<string> {
const formData = new FormData();
formData.append("file", file);
return this._request("POST", "/store/submissions/media", formData);
}
updateStoreProfile(profile: CreatorDetails): Promise<CreatorDetails> {
return this._request("PUT", "/store/profile", profile);
}
///////////////////////////////////////////
/////////// INTERNAL FUNCTIONS ////////////
//////////////////////////////??///////////
private async _get(path: string, query?: Record<string, any>) {
return this._request("GET", path, query);
}

View File

@@ -312,3 +312,93 @@ export type AnalyticsDetails = {
data: { [key: string]: any };
index: string;
};
export type Pagination = {
total_items: number;
total_pages: number;
current_page: number;
page_size: number;
};
export type StoreAgent = {
slug: string;
agent_name: string;
agent_image: string;
creator: string;
creator_avatar: string;
sub_heading: string;
description: string;
runs: number;
rating: number;
};
export type StoreAgentsResponse = {
agents: StoreAgent[];
pagination: Pagination;
};
export type StoreAgentDetails = {
slug: string;
agent_name: string;
agent_video: string;
agent_image: string[];
creator: string;
creator_avatar: string;
sub_heading: string;
description: string;
categories: string[];
runs: number;
rating: number;
versions: string[];
};
export type Creator = {
name: string;
username: string;
description: string;
avatar_url: string;
num_agents: number;
};
export type CreatorsResponse = {
creators: Creator[];
pagination: Pagination;
};
export type CreatorDetails = {
name: string;
username: string;
description: string;
links: string[];
avatar_url: string;
agent_rating: number;
agent_runs: number;
top_categories: string[];
};
export type StoreSubmission = {
name: string;
description: string;
image_urls: string[];
date_submitted: string;
status: string;
runs: number;
rating: number;
};
export type StoreSubmissionsResponse = {
submissions: StoreSubmission[];
pagination: Pagination;
};
export type StoreSubmissionRequest = {
agent_id: string;
agent_version: number;
slug: string;
name: string;
sub_heading: string;
video_url?: string;
image_urls: string[];
description: string;
categories: string[];
};

View File

@@ -31,6 +31,6 @@ export function createServerClient() {
},
);
} catch (error) {
return null;
throw error;
}
}