Files
AutoGPT/autogpt_platform/backend/schema.prisma
Zamil Majdy 7b951c977e feat(platform): implement graph-level Safe Mode toggle for HITL blocks (#11455)
## Summary

This PR implements a graph-level Safe Mode toggle system for
Human-in-the-Loop (HITL) blocks. When Safe Mode is ON (default), HITL
blocks require manual review before proceeding. When OFF, they execute
automatically.

## 🔧 Backend Changes

- **Database**: Added `metadata` JSON column to `AgentGraph` table with
migration
- **API**: Updated `execute_graph` endpoint to accept `safe_mode`
parameter
- **Execution**: Enhanced execution context to use graph metadata as
default with API override capability
- **Auto-detection**: Automatically populate `has_human_in_the_loop` for
graphs containing HITL blocks
- **Block Detection**: HITL block ID:
`8b2a7b3c-6e9d-4a5f-8c1b-2e3f4a5b6c7d`

## 🎨 Frontend Changes

- **Component**: New `FloatingSafeModeToggle` with dual variants:
  - **White variant**: For library pages, integrates with action buttons
  - **Black variant**: For builders, floating positioned  
- **Integration**: Added toggles to both new/legacy builders and library
pages
- **API Integration**: Direct graph metadata updates via
`usePutV1UpdateGraphVersion`
- **Query Management**: React Query cache invalidation for consistent UI
updates
- **Conditional Display**: Toggle only appears when graph contains HITL
blocks

## 🛠 Technical Implementation

- **Safe Mode ON** (default): HITL blocks require manual review before
proceeding
- **Safe Mode OFF**: HITL blocks execute automatically without
intervention
- **Priority**: Backend API `safe_mode` parameter takes precedence over
graph metadata
- **Detection**: Auto-populates `has_human_in_the_loop` metadata field
- **Positioning**: Proper z-index and responsive positioning for
floating elements

## 🚧 Known Issues (Work in Progress)

### High Priority
- [ ] **Toggle state persistence**: Always shows "ON" regardless of
actual state - query invalidation issue
- [ ] **LibraryAgent metadata**: Missing metadata field causing
TypeScript errors
- [ ] **Tooltip z-index**: Still covered by some UI elements despite
high z-index

### Medium Priority  
- [ ] **HITL detection**: Logic needs improvement for reliable block
detection
- [ ] **Error handling**: Removing HITL blocks from graph causes save
errors
- [ ] **TypeScript**: Fix type mismatches between GraphModel and
LibraryAgent

### Low Priority
- [ ] **Frontend API**: Add `safe_mode` parameter to execution calls
once OpenAPI is regenerated
- [ ] **Performance**: Consider debouncing rapid toggle clicks

## 🧪 Test Plan

- [ ] Verify toggle appears only when graph has HITL blocks
- [ ] Test toggle persistence across page refreshes  
- [ ] Confirm API calls update graph metadata correctly
- [ ] Validate execution behavior respects safe mode setting
- [ ] Check styling consistency across builder and library contexts

## 🔗 Related

- Addresses requirements for graph-level HITL configuration
- Builds on existing FloatingReviewsPanel infrastructure
- Integrates with existing graph metadata system

🤖 Generated with [Claude Code](https://claude.ai/code)
2025-12-02 09:55:55 +00:00

959 lines
30 KiB
Plaintext

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
generator client {
provider = "prisma-client-py"
recursive_type_depth = -1
interface = "asyncio"
previewFeatures = ["views", "fullTextSearch"]
partial_type_generator = "backend/data/partial_types.py"
}
// User model to mirror Auth provider users
model User {
id String @id // This should match the Supabase user ID
email String @unique
emailVerified Boolean @default(true)
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
metadata Json @default("{}")
integrations String @default("")
stripeCustomerId String?
topUpConfig Json?
maxEmailsPerDay Int @default(3)
notifyOnAgentRun Boolean @default(true)
notifyOnZeroBalance Boolean @default(true)
notifyOnLowBalance Boolean @default(true)
notifyOnBlockExecutionFailed Boolean @default(true)
notifyOnContinuousAgentError Boolean @default(true)
notifyOnDailySummary Boolean @default(true)
notifyOnWeeklySummary Boolean @default(true)
notifyOnMonthlySummary Boolean @default(true)
notifyOnAgentApproved Boolean @default(true)
notifyOnAgentRejected Boolean @default(true)
timezone String @default("not-set")
// Relations
AgentGraphs AgentGraph[]
AgentGraphExecutions AgentGraphExecution[]
AnalyticsDetails AnalyticsDetails[]
AnalyticsMetrics AnalyticsMetrics[]
CreditTransactions CreditTransaction[]
UserBalance UserBalance?
AgentPresets AgentPreset[]
LibraryAgents LibraryAgent[]
Profile Profile[]
UserOnboarding UserOnboarding?
StoreListings StoreListing[]
StoreListingReviews StoreListingReview[]
StoreVersionsReviewed StoreListingVersion[]
APIKeys APIKey[]
IntegrationWebhooks IntegrationWebhook[]
NotificationBatches UserNotificationBatch[]
PendingHumanReviews PendingHumanReview[]
}
enum OnboardingStep {
// Introductory onboarding (Library)
WELCOME
USAGE_REASON
INTEGRATIONS
AGENT_CHOICE
AGENT_NEW_RUN
AGENT_INPUT
CONGRATS
// First Wins
GET_RESULTS
MARKETPLACE_VISIT
MARKETPLACE_ADD_AGENT
MARKETPLACE_RUN_AGENT
BUILDER_SAVE_AGENT
// Consistency Challenge
RE_RUN_AGENT
SCHEDULE_AGENT
RUN_AGENTS
RUN_3_DAYS
// The Pro Playground
TRIGGER_WEBHOOK
RUN_14_DAYS
RUN_AGENTS_100
// No longer rewarded but exist for analytical purposes
BUILDER_OPEN
BUILDER_RUN_AGENT
}
model UserOnboarding {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
completedSteps OnboardingStep[] @default([])
walletShown Boolean @default(false)
notified OnboardingStep[] @default([])
rewardedFor OnboardingStep[] @default([])
usageReason String?
integrations String[] @default([])
otherIntegrations String?
selectedStoreListingVersionId String?
agentInput Json?
onboardingAgentExecutionId String?
agentRuns Int @default(0)
lastRunAt DateTime?
consecutiveRunDays Int @default(0)
userId String @unique
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
// This model describes the Agent Graph/Flow (Multi Agent System).
model AgentGraph {
id String @default(uuid())
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
name String?
description String?
instructions String?
recommendedScheduleCron String?
isActive Boolean @default(true)
// Link to User model
userId String
// FIX: 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
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
forkedFromId String?
forkedFromVersion Int?
forkedFrom AgentGraph? @relation("AgentGraphForks", fields: [forkedFromId, forkedFromVersion], references: [id, version])
forks AgentGraph[] @relation("AgentGraphForks")
Nodes AgentNode[]
Executions AgentGraphExecution[]
Presets AgentPreset[]
LibraryAgents LibraryAgent[]
StoreListings StoreListing[]
StoreListingVersions StoreListingVersion[]
@@id(name: "graphVersionId", [id, version])
@@index([userId, isActive, id, version])
@@index([forkedFromId, forkedFromVersion])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////////////// 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())
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
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
agentGraphId String
agentGraphVersion Int
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Restrict)
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
Executions AgentGraphExecution[]
// For webhook-triggered agents: reference to the webhook that triggers the agent
webhookId String?
Webhook IntegrationWebhook? @relation(fields: [webhookId], references: [id])
isDeleted Boolean @default(false)
@@index([userId])
@@index([agentGraphId, agentGraphVersion])
@@index([webhookId])
}
enum NotificationType {
AGENT_RUN
ZERO_BALANCE
LOW_BALANCE
BLOCK_EXECUTION_FAILED
CONTINUOUS_AGENT_ERROR
DAILY_SUMMARY
WEEKLY_SUMMARY
MONTHLY_SUMMARY
REFUND_REQUEST
REFUND_PROCESSED
AGENT_APPROVED
AGENT_REJECTED
}
model NotificationEvent {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
UserNotificationBatch UserNotificationBatch? @relation(fields: [userNotificationBatchId], references: [id])
userNotificationBatchId String?
type NotificationType
data Json
@@index([userNotificationBatchId])
}
model UserNotificationBatch {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
type NotificationType
Notifications NotificationEvent[]
// Each user can only have one batch of a notification type at a time
@@unique([userId, type])
}
// For the library page
// It is a user controlled list of agents, that they will see in their library
model LibraryAgent {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
imageUrl String?
agentGraphId String
agentGraphVersion Int
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Restrict)
creatorId String?
Creator Profile? @relation(fields: [creatorId], references: [id])
useGraphIsActiveVersion Boolean @default(false)
isFavorite Boolean @default(false)
isCreatedByUser Boolean @default(false)
isArchived Boolean @default(false)
isDeleted Boolean @default(false)
settings Json @default("{}")
@@unique([userId, agentGraphId, agentGraphVersion])
@@index([agentGraphId, agentGraphVersion])
@@index([creatorId])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////// 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())
agentBlockId String
AgentBlock AgentBlock @relation(fields: [agentBlockId], references: [id], onUpdate: Cascade)
agentGraphId String
agentGraphVersion Int @default(1)
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], 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")
constantInput Json @default("{}")
// For webhook-triggered blocks: reference to the webhook that triggers the node
webhookId String?
Webhook IntegrationWebhook? @relation(fields: [webhookId], references: [id])
metadata Json @default("{}")
Executions AgentNodeExecution[]
@@index([agentGraphId, agentGraphVersion])
@@index([agentBlockId])
@@index([webhookId])
}
// This model describes the link between two AgentNodes.
model AgentNodeLink {
id String @id @default(uuid())
// Output of a node is connected to the source of the link.
agentNodeSourceId String
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
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)
@@index([agentNodeSourceId])
@@index([agentNodeSinkId])
}
// This model describes a component that will be executed by the AgentNode.
model AgentBlock {
id String @id @default(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 String
outputSchema String
// Prisma requires explicit back-references.
ReferencedByAgentNode AgentNode[]
}
// This model describes the status of an AgentGraphExecution or AgentNodeExecution.
enum AgentExecutionStatus {
INCOMPLETE
QUEUED
RUNNING
COMPLETED
TERMINATED
FAILED
REVIEW
}
// This model describes the execution of an AgentGraph.
model AgentGraphExecution {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
startedAt DateTime?
isDeleted Boolean @default(false)
executionStatus AgentExecutionStatus @default(COMPLETED)
agentGraphId String
agentGraphVersion Int @default(1)
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Cascade)
agentPresetId String?
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
inputs Json?
credentialInputs Json?
nodesInputMasks Json?
NodeExecutions AgentNodeExecution[]
// Link to User model -- Executed by this user
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
stats Json?
// Parent-child execution tracking for nested agent graphs
parentGraphExecutionId String?
ParentExecution AgentGraphExecution? @relation("ParentChildExecution", fields: [parentGraphExecutionId], references: [id], onDelete: SetNull)
ChildExecutions AgentGraphExecution[] @relation("ParentChildExecution")
// Sharing fields
isShared Boolean @default(false)
shareToken String? @unique
sharedAt DateTime?
PendingHumanReviews PendingHumanReview[]
@@index([agentGraphId, agentGraphVersion])
@@index([userId, isDeleted, createdAt])
@@index([createdAt])
@@index([agentPresetId])
@@index([shareToken])
@@index([parentGraphExecutionId])
}
// This model describes the execution of an AgentNode.
model AgentNodeExecution {
id String @id @default(uuid())
agentGraphExecutionId String
GraphExecution AgentGraphExecution @relation(fields: [agentGraphExecutionId], references: [id], onDelete: Cascade)
agentNodeId String
Node AgentNode @relation(fields: [agentNodeId], references: [id], onDelete: Cascade)
Input AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionInput")
Output AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionOutput")
executionStatus AgentExecutionStatus @default(COMPLETED)
executionData Json?
addedTime DateTime @default(now())
queuedTime DateTime?
startedTime DateTime?
endedTime DateTime?
stats Json?
PendingHumanReview PendingHumanReview?
@@index([agentGraphExecutionId, agentNodeId, executionStatus])
@@index([agentNodeId, executionStatus])
@@index([addedTime, queuedTime])
}
// This model describes the output of an AgentNodeExecution.
model AgentNodeExecutionInputOutput {
id String @id @default(uuid())
name String
data Json?
time DateTime @default(now())
// Prisma requires explicit back-references.
referencedByInputExecId String?
ReferencedByInputExec AgentNodeExecution? @relation("AgentNodeExecutionInput", fields: [referencedByInputExecId], references: [id], onDelete: Cascade)
referencedByOutputExecId String?
ReferencedByOutputExec AgentNodeExecution? @relation("AgentNodeExecutionOutput", fields: [referencedByOutputExecId], references: [id], onDelete: Cascade)
agentPresetId String?
AgentPreset AgentPreset? @relation("AgentPresetsInputData", fields: [agentPresetId], references: [id])
// Input and Output pin names are unique for each AgentNodeExecution.
@@unique([referencedByInputExecId, referencedByOutputExecId, name])
@@index([referencedByOutputExecId])
// Composite index for `upsert_execution_input`.
@@index([name, time])
@@index([agentPresetId])
}
model AgentNodeExecutionKeyValueData {
userId String
key String
agentNodeExecutionId String
data Json?
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
@@id([userId, key])
}
enum ReviewStatus {
WAITING
APPROVED
REJECTED
}
// Pending human reviews for Human-in-the-loop blocks
model PendingHumanReview {
nodeExecId String @id
userId String
graphExecId String
graphId String
graphVersion Int
payload Json // The actual payload data to be reviewed
instructions String? // Instructions/message for the reviewer
editable Boolean @default(true) // Whether the reviewer can edit the data
status ReviewStatus @default(WAITING)
reviewMessage String? // Optional message from the reviewer
wasEdited Boolean? // Whether the data was modified during review
processed Boolean @default(false) // Whether the review result has been processed by the execution engine
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
reviewedAt DateTime?
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
NodeExecution AgentNodeExecution @relation(fields: [nodeExecId], references: [id], onDelete: Cascade)
GraphExecution AgentGraphExecution @relation(fields: [graphExecId], references: [id], onDelete: Cascade)
@@unique([nodeExecId]) // One pending review per node execution
@@index([userId, status])
@@index([graphExecId, status])
}
// Webhook that is registered with a provider and propagates to one or more nodes
model IntegrationWebhook {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
userId String
User User @relation(fields: [userId], references: [id], onDelete: Restrict) // Webhooks must be deregistered before deleting
provider String // e.g. 'github'
credentialsId String // relation to the credentials that the webhook was created with
webhookType String // e.g. 'repo'
resource String // e.g. 'Significant-Gravitas/AutoGPT'
events String[] // e.g. ['created', 'updated']
config Json
secret String // crypto string, used to verify payload authenticity
providerWebhookId String // Webhook ID assigned by the provider
AgentNodes AgentNode[]
AgentPresets AgentPreset[]
}
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()"))
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// Link to User model
userId String
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?
// Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc.
dataIndex String?
@@index([userId, type])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// METRICS TRACKING TABLES ////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model AnalyticsMetrics {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @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
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////// ACCOUNTING AND CREDIT SYSTEM TABLES //////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
enum CreditTransactionType {
TOP_UP
USAGE
GRANT
REFUND
CARD_CHECK
}
model CreditTransaction {
transactionKey String @default(uuid())
createdAt DateTime @default(now())
userId String
User User? @relation(fields: [userId], references: [id], onDelete: NoAction)
amount Int
type CreditTransactionType
runningBalance Int?
isActive Boolean @default(true)
metadata Json?
@@id(name: "creditTransactionIdentifier", [transactionKey, userId])
@@index([userId, createdAt])
}
enum CreditRefundRequestStatus {
PENDING
APPROVED
REJECTED
}
model CreditRefundRequest {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
transactionKey String
amount Int
reason String
result String?
status CreditRefundRequestStatus @default(PENDING)
@@index([userId, transactionKey])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// Store TABLES ///////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model SearchTerms {
// User ID not being logged as this is anonymous analytics data
// Not using uuid as we want to minimise table size
id BigInt @id @default(autoincrement())
createdDate DateTime
searchTerm String
@@index([createdDate])
}
model Profile {
id String @id @default(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?
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
name String
username String @unique
description String
links String[]
avatarUrl String?
isFeatured Boolean @default(false)
LibraryAgents LibraryAgent[]
@@index([userId])
}
view Creator {
username String @unique
name String
avatar_url String
description String
top_categories String[]
links String[]
num_agents Int
agent_rating Float
agent_runs Int
is_featured Boolean
// Materialized views used (refreshed every 15 minutes via pg_cron):
// - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId
// * idx_mv_agent_run_counts (UNIQUE on agentGraphId) - Primary lookup
// - mv_review_stats - Pre-aggregated review statistics (count, avg rating) by storeListingId
// * idx_mv_review_stats (UNIQUE on storeListingId) - Primary lookup
// * idx_mv_review_stats_rating (avg_rating DESC) - Sort by rating performance
// * idx_mv_review_stats_count (review_count DESC) - Sort by review count performance
//
// Query strategy: Uses CTEs to efficiently aggregate creator statistics leveraging materialized views
}
view StoreAgent {
listing_id String @id
storeListingVersionId String
updated_at DateTime
slug String
agent_name String
agent_video String?
agent_image String[]
featured Boolean @default(false)
creator_username String?
creator_avatar String?
sub_heading String
description String
categories String[]
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
runs Int
rating Float
versions String[]
is_available Boolean @default(true)
useForOnboarding Boolean @default(false)
// Materialized views used (refreshed every 15 minutes via pg_cron):
// - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId
// * idx_mv_agent_run_counts (UNIQUE on agentGraphId) - Primary lookup
// - mv_review_stats - Pre-aggregated review statistics (count, avg rating) by storeListingId
// * idx_mv_review_stats (UNIQUE on storeListingId) - Primary lookup
// * idx_mv_review_stats_rating (avg_rating DESC) - Sort by rating performance
// * idx_mv_review_stats_count (review_count DESC) - Sort by review count performance
//
// Query strategy: Uses CTE for version aggregation and joins with materialized views for performance
}
view StoreSubmission {
listing_id String @id
user_id String
slug String
name String
sub_heading String
description String
image_urls String[]
date_submitted DateTime
status SubmissionStatus
runs Int
rating Float
agent_id String
agent_version Int
store_listing_version_id String
reviewer_id String?
review_comments String?
internal_comments String?
reviewed_at DateTime?
changes_summary String?
video_url String?
categories String[]
// Index or unique are not applied to views
}
// Note: This is actually a MATERIALIZED VIEW in the database
// Refreshed automatically every 15 minutes via pg_cron (with fallback to manual refresh)
view mv_agent_run_counts {
agentGraphId String @unique
run_count Int
// Pre-aggregated count of AgentGraphExecution records by agentGraphId
// Used by StoreAgent and Creator views for performance optimization
// Unique index created automatically on agentGraphId for fast lookups
// Refresh uses CONCURRENTLY to avoid blocking reads
}
// Note: This is actually a MATERIALIZED VIEW in the database
// Refreshed automatically every 15 minutes via pg_cron (with fallback to manual refresh)
view mv_review_stats {
storeListingId String @unique
review_count Int
avg_rating Float
// Pre-aggregated review statistics from StoreListingReview
// Includes count of reviews and average rating per StoreListing
// Only includes approved versions (submissionStatus = 'APPROVED') and non-deleted listings
// Used by StoreAgent view for performance optimization
// Unique index created automatically on storeListingId for fast lookups
// Refresh uses CONCURRENTLY to avoid blocking reads
}
model StoreListing {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
isDeleted Boolean @default(false)
// Whether any version has been approved and is available for display
hasApprovedVersion Boolean @default(false)
// URL-friendly identifier for this agent (moved from StoreListingVersion)
slug String
// Allow this agent to be used during onboarding
useForOnboarding Boolean @default(false)
// The currently active version that should be shown to users
activeVersionId String? @unique
ActiveVersion StoreListingVersion? @relation("ActiveVersion", fields: [activeVersionId], references: [id])
// The agent link here is only so we can do lookup on agentId
agentGraphId String
agentGraphVersion Int
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Cascade)
owningUserId String
OwningUser User @relation(fields: [owningUserId], references: [id])
// Relations
Versions StoreListingVersion[] @relation("ListingVersions")
// Unique index on agentId to ensure only one listing per agent, regardless of number of versions the agent has.
@@unique([agentGraphId])
@@unique([owningUserId, slug])
// Used in the view query
@@index([isDeleted, hasApprovedVersion])
@@index([agentGraphId, agentGraphVersion])
}
model StoreListingVersion {
id String @id @default(uuid())
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// The agent and version to be listed on the store
agentGraphId String
agentGraphVersion Int
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version])
// Content fields
name String
subHeading String
videoUrl String?
imageUrls String[]
description String
instructions 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)
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
// Version workflow state
submissionStatus SubmissionStatus @default(DRAFT)
submittedAt DateTime?
// Relations
storeListingId String
StoreListing StoreListing @relation("ListingVersions", fields: [storeListingId], references: [id], onDelete: Cascade)
// This version might be the active version for a listing
ActiveFor StoreListing? @relation("ActiveVersion")
// Submission history
changesSummary String?
// Review information
reviewerId String?
Reviewer User? @relation(fields: [reviewerId], references: [id])
internalComments String? // Private notes for admin use only
reviewComments String? // Comments visible to creator
reviewedAt DateTime?
recommendedScheduleCron String? // cron expression like "0 9 * * *"
// Reviews for this specific version
Reviews StoreListingReview[]
@@unique([storeListingId, version])
@@index([storeListingId, submissionStatus, isAvailable])
@@index([submissionStatus])
@@index([reviewerId])
@@index([agentGraphId, agentGraphVersion]) // Non-unique index for efficient lookups
}
model StoreListingReview {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
storeListingVersionId String
StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade)
reviewByUserId String
ReviewByUser User @relation(fields: [reviewByUserId], references: [id])
score Int
comments String?
@@unique([storeListingVersionId, reviewByUserId])
@@index([reviewByUserId])
}
enum SubmissionStatus {
DRAFT // Being prepared, not yet submitted
PENDING // Submitted, awaiting review
APPROVED // Reviewed and approved
REJECTED // Reviewed and rejected
}
enum APIKeyPermission {
EXECUTE_GRAPH // Can execute agent graphs
READ_GRAPH // Can get graph versions and details
EXECUTE_BLOCK // Can execute individual blocks
READ_BLOCK // Can get block information
READ_STORE // Can read store agents and creators
USE_TOOLS // Can use chat tools via external API
}
model APIKey {
id String @id @default(uuid())
name String
head String // First few chars for identification
tail String
hash String @unique
salt String? // null for legacy unsalted keys
status APIKeyStatus @default(ACTIVE)
permissions APIKeyPermission[]
createdAt DateTime @default(now())
lastUsedAt DateTime?
revokedAt DateTime?
description String?
// Relation to user
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([head, name])
@@index([userId, status])
}
model UserBalance {
userId String @id
balance Int @default(0)
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
enum APIKeyStatus {
ACTIVE
REVOKED
SUSPENDED
}