feat(platform): updated schema to allow featuring of specific creators (#9048)

updated schema to allow featuring of specific creators
This commit is contained in:
Swifty 2024-12-18 14:32:03 +01:00 committed by GitHub
parent e8dd0a297e
commit aa883d8465
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 28 additions and 629 deletions

View File

@ -153,6 +153,7 @@ async def test_add_agent_to_library(mocker):
)
mock_user_agent = mocker.patch("prisma.models.UserAgent.prisma")
mock_user_agent.return_value.find_first = mocker.AsyncMock(return_value=None)
mock_user_agent.return_value.create = mocker.AsyncMock()
# Call function
@ -162,6 +163,13 @@ async def test_add_agent_to_library(mocker):
mock_store_listing_version.return_value.find_unique.assert_called_once_with(
where={"id": "version123"}, include={"Agent": True}
)
mock_user_agent.return_value.find_first.assert_called_once_with(
where={
"userId": "test-user",
"agentId": "agent1",
"agentVersion": 1,
}
)
mock_user_agent.return_value.create.assert_called_once_with(
data=prisma.types.UserAgentCreateInput(
userId="test-user", agentId="agent1", agentVersion=1, isCreatedByUser=False

View File

@ -172,6 +172,9 @@ async def get_store_creators(
# Build where clause with sanitized inputs
where = {}
if featured:
where["isFeatured"] = featured
# Add search filter if provided, using parameterized queries
if search_query:
# Sanitize and validate search query by escaping special characters
@ -247,6 +250,7 @@ async def get_store_creators(
num_agents=creator.num_agents,
agent_rating=creator.agent_rating,
agent_runs=creator.agent_runs,
is_featured=creator.is_featured,
)
for creator in creators
]

View File

@ -113,6 +113,7 @@ async def test_get_store_creator_details(mocker):
agent_rating=4.5,
agent_runs=10,
top_categories=["test"],
is_featured=False,
)
# Mock prisma call
@ -197,6 +198,7 @@ async def test_update_profile(mocker):
description="Test description",
links=["link1"],
avatarUrl="avatar.jpg",
isFeatured=False,
createdAt=datetime.now(),
updatedAt=datetime.now(),
)
@ -215,6 +217,7 @@ async def test_update_profile(mocker):
description="Test description",
links=["link1"],
avatar_url="avatar.jpg",
is_featured=False,
)
# Call function
@ -239,6 +242,7 @@ async def test_get_user_profile(mocker):
description="Test description",
links=["link1", "link2"],
avatarUrl="avatar.jpg",
isFeatured=False,
createdAt=datetime.now(),
updatedAt=datetime.now(),
)

View File

@ -74,6 +74,7 @@ class Creator(pydantic.BaseModel):
num_agents: int
agent_rating: float
agent_runs: int
is_featured: bool
class CreatorsResponse(pydantic.BaseModel):
@ -98,6 +99,7 @@ class Profile(pydantic.BaseModel):
description: str
links: list[str]
avatar_url: str
is_featured: bool
class StoreSubmission(pydantic.BaseModel):

View File

@ -88,6 +88,7 @@ def test_creator():
description="Test description",
avatar_url="avatar.jpg",
num_agents=5,
is_featured=False,
)
assert creator.name == "Test Creator"
assert creator.num_agents == 5
@ -104,6 +105,7 @@ def test_creators_response():
description="Test description",
avatar_url="avatar.jpg",
num_agents=5,
is_featured=False,
)
],
pagination=backend.server.v2.store.model.Pagination(

View File

@ -402,6 +402,7 @@ def test_get_creators_pagination(mocker: pytest_mock.MockFixture):
num_agents=1,
agent_rating=4.5,
agent_runs=100,
is_featured=False,
)
for i in range(5)
],

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Profile" ADD COLUMN "isFeatured" BOOLEAN NOT NULL DEFAULT false;

View File

@ -71,6 +71,7 @@ SELECT
p.description,
ARRAY_AGG(DISTINCT c) FILTER (WHERE c IS NOT NULL) as top_categories,
p.links,
p."isFeatured" as is_featured,
COALESCE(ast.num_agents, 0) as num_agents,
COALESCE(ast.agent_rating, 0.0) as agent_rating,
COALESCE(ast.agent_runs, 0) as agent_runs
@ -84,7 +85,7 @@ LEFT JOIN LATERAL (
AND sl."isDeleted" = FALSE
AND sl."isApproved" = TRUE
) cats ON true
GROUP BY p.username, p.name, p."avatarUrl", p.description, p.links,
GROUP BY p.username, p.name, p."avatarUrl", p.description, p.links, p."isFeatured",
ast.num_agents, ast.agent_rating, ast.agent_runs;
CREATE VIEW "StoreSubmission" AS

View File

@ -423,6 +423,8 @@ model Profile {
avatarUrl String?
isFeatured Boolean @default(false)
@@index([username])
@@index([userId])
}
@ -439,6 +441,7 @@ view Creator {
num_agents Int
agent_rating Float
agent_runs Int
is_featured Boolean
}
view StoreAgent {

View File

@ -1,628 +0,0 @@
// We need to migrate our database schema to support the domain as we understand it now
// To do so requires adding a bunch of new tables, but also modiftying old ones and how
// they relate to each other. This is a large change, so instead of doing in in one go,
// We have created the target schema, and will migrate to it incrementally.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-py"
recursive_type_depth = 5
interface = "asyncio"
}
// User model to mirror Auth provider users
model User {
id String @id @db.Uuid // This should match the Supabase user ID
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
metadata String @default("")
// Relations
Agents Agent[]
AgentExecutions AgentExecution[]
AgentExecutionSchedules AgentExecutionSchedule[]
AnalyticsDetails AnalyticsDetails[]
AnalyticsMetrics AnalyticsMetrics[]
UserBlockCredit UserBlockCredit[]
AgentPresets AgentPreset[]
UserAgents UserAgent[]
// User Group relations
UserGroupMemberships UserGroupMembership[]
Profile Profile[]
StoreListing StoreListing[]
StoreListingSubmission StoreListingSubmission[]
StoreListingReview StoreListingReview[]
}
model UserGroup {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
name String
description String
groupIconUrl String?
UserGroupMemberships UserGroupMembership[]
Agents Agent[]
Profile Profile[]
StoreListing StoreListing[]
@@index([name])
}
enum UserGroupRole {
MEMBER
OWNER
}
model UserGroupMembership {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String @db.Uuid
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
userGroupId String @db.Uuid
UserGroup UserGroup @relation(fields: [userGroupId], references: [id], onDelete: Cascade)
Role UserGroupRole @default(MEMBER)
@@unique([userId, userGroupId])
@@index([userId])
@@index([userGroupId])
}
// This model describes the Agent Graph/Flow (Multi Agent System).
model Agent {
id String @default(uuid()) @db.Uuid
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
name String?
description String?
// Link to User model
createdByUserId String? @db.Uuid
// Do not cascade delete the agent when the user is deleted
// This allows us to delete user data with deleting the agent which maybe in use by other users
CreatedByUser User? @relation(fields: [createdByUserId], references: [id], onDelete: SetNull)
groupId String? @db.Uuid
// Do not cascade delete the agent when the group is deleted
// This allows us to delete user group data with deleting the agent which maybe in use by other users
Group UserGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
AgentNodes AgentNode[]
AgentExecution AgentExecution[]
// All sub-graphs are defined within this 1-level depth list (even if it's a nested graph).
SubAgents Agent[] @relation("SubAgents")
agentParentId String? @db.Uuid
agentParentVersion Int?
AgentParent Agent? @relation("SubAgents", fields: [agentParentId, agentParentVersion], references: [id, version])
AgentPresets AgentPreset[]
WebhookTrigger WebhookTrigger[]
AgentExecutionSchedule AgentExecutionSchedule[]
UserAgents UserAgent[]
UserBlockCredit UserBlockCredit[]
StoreListing StoreListing[]
StoreListingVersion StoreListingVersion[]
@@id(name: "agentVersionId", [id, version])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////////////// USER SPECIFIC DATA ////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
// An AgentPrest is an Agent + User Configuration of that agent.
// For example, if someone has created a weather agent and they want to set it up to
// Inform them of extreme weather warnings in Texas, the agent with the configuration to set it to
// monitor texas, along with the cron setup or webhook tiggers, is an AgentPreset
model AgentPreset {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
name String
description String
// For agents that can be triggered by webhooks or cronjob
// This bool allows us to disable a configured agent without deleting it
isActive Boolean @default(true)
userId String @db.Uuid
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
agentId String @db.Uuid
agentVersion Int
Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
UserAgents UserAgent[]
WebhookTrigger WebhookTrigger[]
AgentExecutionSchedule AgentExecutionSchedule[]
AgentExecution AgentExecution[]
@@index([userId])
}
// For the library page
// It is a user controlled list of agents, that they will see in there library
model UserAgent {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String @db.Uuid
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
agentId String @db.Uuid
agentVersion Int
Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version])
agentPresetId String? @db.Uuid
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
isFavorite Boolean @default(false)
isCreatedByUser Boolean @default(false)
isArchived Boolean @default(false)
isDeleted Boolean @default(false)
@@index([userId])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////// AGENT DEFINITION AND EXECUTION TABLES ////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
// This model describes a single node in the Agent Graph/Flow (Multi Agent System).
model AgentNode {
id String @id @default(uuid()) @db.Uuid
agentBlockId String @db.Uuid
AgentBlock AgentBlock @relation(fields: [agentBlockId], references: [id], onUpdate: Cascade)
agentId String @db.Uuid
agentVersion Int @default(1)
Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
// List of consumed input, that the parent node should provide.
Input AgentNodeLink[] @relation("AgentNodeSink")
// List of produced output, that the child node should be executed.
Output AgentNodeLink[] @relation("AgentNodeSource")
// JSON serialized dict[str, str] containing predefined input values.
constantInput Json @default("{}")
// JSON serialized dict[str, str] containing the node metadata.
metadata Json @default("{}")
ExecutionHistory AgentNodeExecution[]
}
// This model describes the link between two AgentNodes.
model AgentNodeLink {
id String @id @default(uuid()) @db.Uuid
// Output of a node is connected to the source of the link.
agentNodeSourceId String @db.Uuid
AgentNodeSource AgentNode @relation("AgentNodeSource", fields: [agentNodeSourceId], references: [id], onDelete: Cascade)
sourceName String
// Input of a node is connected to the sink of the link.
agentNodeSinkId String @db.Uuid
AgentNodeSink AgentNode @relation("AgentNodeSink", fields: [agentNodeSinkId], references: [id], onDelete: Cascade)
sinkName String
// Default: the data coming from the source can only be consumed by the sink once, Static: input data will be reused.
isStatic Boolean @default(false)
}
// This model describes a component that will be executed by the AgentNode.
model AgentBlock {
id String @id @default(uuid()) @db.Uuid
name String @unique
// We allow a block to have multiple types of input & output.
// Serialized object-typed `jsonschema` with top-level properties as input/output name.
inputSchema Json @default("{}")
outputSchema Json @default("{}")
// Prisma requires explicit back-references.
ReferencedByAgentNode AgentNode[]
UserBlockCredit UserBlockCredit[]
}
// This model describes the status of an AgentExecution or AgentNodeExecution.
enum AgentExecutionStatus {
INCOMPLETE
QUEUED
RUNNING
COMPLETED
FAILED
}
// Enum for execution trigger types
enum ExecutionTriggerType {
MANUAL
SCHEDULE
WEBHOOK
}
// This model describes the execution of an AgentGraph.
model AgentExecution {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
startedAt DateTime?
executionTriggerType ExecutionTriggerType @default(MANUAL)
executionStatus AgentExecutionStatus @default(COMPLETED)
agentId String @db.Uuid
agentVersion Int @default(1)
Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
// we need to be able to associate an agent execution with an agent preset
agentPresetId String? @db.Uuid
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
AgentNodeExecutions AgentNodeExecution[]
// This is so we can track which user executed the agent.
executedByUserId String @db.Uuid
ExecutedByUser User @relation(fields: [executedByUserId], references: [id], onDelete: Cascade)
stats Json @default("{}") // JSON serialized object
}
// This model describes the execution of an AgentNode.
model AgentNodeExecution {
id String @id @default(uuid()) @db.Uuid
agentExecutionId String @db.Uuid
AgentExecution AgentExecution @relation(fields: [agentExecutionId], references: [id], onDelete: Cascade)
agentNodeId String @db.Uuid
AgentNode AgentNode @relation(fields: [agentNodeId], references: [id], onDelete: Cascade)
Input AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionInput")
Output AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionOutput")
executionStatus AgentExecutionStatus @default(COMPLETED)
// Final JSON serialized input data for the node execution.
executionData String?
addedTime DateTime @default(now())
queuedTime DateTime?
startedTime DateTime?
endedTime DateTime?
stats Json @default("{}") // JSON serialized object
UserBlockCredit UserBlockCredit[]
}
// This model describes the output of an AgentNodeExecution.
model AgentNodeExecutionInputOutput {
id String @id @default(uuid()) @db.Uuid
name String
data String
time DateTime @default(now())
// Prisma requires explicit back-references.
referencedByInputExecId String? @db.Uuid
ReferencedByInputExec AgentNodeExecution? @relation("AgentNodeExecutionInput", fields: [referencedByInputExecId], references: [id], onDelete: Cascade)
referencedByOutputExecId String? @db.Uuid
ReferencedByOutputExec AgentNodeExecution? @relation("AgentNodeExecutionOutput", fields: [referencedByOutputExecId], references: [id], onDelete: Cascade)
agentPresetId String? @db.Uuid
AgentPreset AgentPreset? @relation("AgentPresetsInputData", fields: [agentPresetId], references: [id])
// Input and Output pin names are unique for each AgentNodeExecution.
@@unique([referencedByInputExecId, referencedByOutputExecId, name])
}
// This model describes the recurring execution schedule of an Agent.
model AgentExecutionSchedule {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
agentPresetId String @db.Uuid
AgentPreset AgentPreset @relation(fields: [agentPresetId], references: [id], onDelete: Cascade)
schedule String // cron expression
isEnabled Boolean @default(true)
// Allows triggers to be routed down different execution paths in an agent graph
triggerIdentifier String
// default and set the value on each update, lastUpdated field has no time zone.
lastUpdated DateTime @default(now()) @updatedAt
// Link to User model
userId String @db.Uuid
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
Agent Agent? @relation(fields: [agentId, agentVersion], references: [id, version])
agentId String? @db.Uuid
agentVersion Int?
@@index([isEnabled])
}
enum HttpMethod {
GET
POST
PUT
DELETE
PATCH
}
model WebhookTrigger {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
agentPresetId String @db.Uuid
AgentPreset AgentPreset @relation(fields: [agentPresetId], references: [id])
method HttpMethod
urlSlug String
// Allows triggers to be routed down different execution paths in an agent graph
triggerIdentifier String
isActive Boolean @default(true)
lastReceivedDataAt DateTime?
isDeleted Boolean @default(false)
Agent Agent? @relation(fields: [agentId, agentVersion], references: [id, version])
agentId String? @db.Uuid
agentVersion Int?
@@index([agentPresetId])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// METRICS TRACKING TABLES ////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model AnalyticsDetails {
// PK uses gen_random_uuid() to allow the db inserts to happen outside of prisma
// typical uuid() inserts are handled by prisma
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// Link to User model
userId String @db.Uuid
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Analytics Categorical data used for filtering (indexable w and w/o userId)
type String
// Analytic Specific Data. We should use a union type here, but prisma doesn't support it.
data Json @default("{}")
// Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc.
dataIndex String?
@@index([userId, type], name: "analyticsDetails")
@@index([type])
}
model AnalyticsMetrics {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// Analytics Categorical data used for filtering (indexable w and w/o userId)
analyticMetric String
// Any numeric data that should be counted upon, summed, or otherwise aggregated.
value Float
// Any string data that should be used to identify the metric as distinct.
// ex: '/build' vs '/market'
dataString String?
// Link to User model
userId String @db.Uuid
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////// ACCOUNTING AND CREDIT SYSTEM TABLES //////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
enum UserBlockCreditType {
TOP_UP
USAGE
}
model UserBlockCredit {
transactionKey String @default(uuid())
createdAt DateTime @default(now())
userId String @db.Uuid
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
blockId String? @db.Uuid
Block AgentBlock? @relation(fields: [blockId], references: [id])
// We need to be able to associate a credit transaction with an agent
executedAgentId String? @db.Uuid
executedAgentVersion Int?
ExecutedAgent Agent? @relation(fields: [executedAgentId, executedAgentVersion], references: [id, version])
// We need to be able to associate a cost with a specific agent execution
agentNodeExecutionId String? @db.Uuid
AgentNodeExecution AgentNodeExecution? @relation(fields: [agentNodeExecutionId], references: [id])
amount Int
type UserBlockCreditType
isActive Boolean @default(true)
metadata Json @default("{}")
@@id(name: "creditTransactionIdentifier", [transactionKey, userId])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// Store TABLES ///////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model Profile {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// Only 1 of user or group can be set.
// The user this profile belongs to, if any.
userId String? @db.Uuid
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
// The group this profile belongs to, if any.
groupId String? @db.Uuid
Group UserGroup? @relation(fields: [groupId], references: [id])
username String @unique
description String
links String[]
avatarUrl String?
@@index([username])
}
model StoreListing {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
isDeleted Boolean @default(false)
// Not needed but makes lookups faster
isApproved Boolean @default(false)
// The agent link here is only so we can do lookup on agentId, for the listing the StoreListingVersion is used.
agentId String @db.Uuid
agentVersion Int
Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
owningUserId String @db.Uuid
OwningUser User @relation(fields: [owningUserId], references: [id])
isGroupListing Boolean @default(false)
owningGroupId String? @db.Uuid
OwningGroup UserGroup? @relation(fields: [owningGroupId], references: [id])
StoreListingVersions StoreListingVersion[]
StoreListingSubmission StoreListingSubmission[]
@@index([isApproved])
@@index([agentId])
@@index([owningUserId])
@@index([owningGroupId])
}
model StoreListingVersion {
id String @id @default(uuid()) @db.Uuid
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// The agent and version to be listed on the store
agentId String @db.Uuid
agentVersion Int
Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version])
// The detials for this version of the agent, this allows the author to update the details of the agent,
// But still allow using old versions of the agent with there original details.
// TODO: Create a database view that shows only the latest version of each store listing.
slug String
name String
videoUrl String?
imageUrls String[]
description String
categories String[]
isFeatured Boolean @default(false)
isDeleted Boolean @default(false)
// Old versions can be made unavailable by the author if desired
isAvailable Boolean @default(true)
// Not needed but makes lookups faster
isApproved Boolean @default(false)
StoreListing StoreListing? @relation(fields: [storeListingId], references: [id], onDelete: Cascade)
storeListingId String? @db.Uuid
StoreListingSubmission StoreListingSubmission[]
// Reviews are on a specific version, but then aggregated up to the listing.
// This allows us to provide a review filter to current version of the agent.
StoreListingReview StoreListingReview[]
@@unique([agentId, agentVersion])
@@index([agentId, agentVersion, isApproved])
}
model StoreListingReview {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
storeListingVersionId String @db.Uuid
StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade)
reviewByUserId String @db.Uuid
ReviewByUser User @relation(fields: [reviewByUserId], references: [id])
score Int
comments String?
}
enum SubmissionStatus {
DAFT
PENDING
APPROVED
REJECTED
}
model StoreListingSubmission {
id String @id @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
storeListingId String @db.Uuid
StoreListing StoreListing @relation(fields: [storeListingId], references: [id], onDelete: Cascade)
storeListingVersionId String @db.Uuid
StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade)
reviewerId String @db.Uuid
Reviewer User @relation(fields: [reviewerId], references: [id])
Status SubmissionStatus @default(PENDING)
reviewComments String?
@@index([storeListingId])
@@index([Status])
}