From c7cd6dc0268d39d0cb9c2cac5e6b846c3f1f709a Mon Sep 17 00:00:00 2001 From: Swifty Date: Tue, 29 Oct 2024 08:22:35 +0100 Subject: [PATCH] feat(platform): Add target database model to reflect domain (#8375) --- autogpt_platform/backend/target.prisma | 628 +++++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 autogpt_platform/backend/target.prisma diff --git a/autogpt_platform/backend/target.prisma b/autogpt_platform/backend/target.prisma new file mode 100644 index 0000000000..7c378b5a6f --- /dev/null +++ b/autogpt_platform/backend/target.prisma @@ -0,0 +1,628 @@ +// 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 Json? @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]) +}