Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/commons-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ POSTGRES_PASSWORD=""
WALLET_PRIVATE_KEY=""

OPENAI_API_KEY=""
# Unified Canvas providers. Model availability is derived from these keys at
# runtime; missing providers remain visible in the catalog but disabled.
GOOGLE_API_KEY=""
KLING_ACCESS_KEY=""
KLING_SECRET_KEY=""
BYTEPLUS_ARK_API_KEY=""
# Optional JSON maps keyed by Commons modelKey (preferred) or provider modelId.
# Required for any catalog entry whose public provider tariff is unavailable.
GOOGLE_MEDIA_PRICE_USD_JSON=""
KLING_MEDIA_PRICE_USD_JSON=""
BYTEPLUS_MEDIA_PRICE_USD_JSON=""
MEDIA_RESERVATION_TTL_SECONDS="7200"
# Optional Knowledge Space retrieval overrides. Lexical + graph search remains
# available when embeddings are disabled or the OpenAI key is absent.
BRAIN_EMBEDDING_MODEL="text-embedding-3-small"
Expand Down
112 changes: 112 additions & 0 deletions apps/commons-api/migrations/versioned/032_canvas_media_studio.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
CREATE TABLE IF NOT EXISTS canvas_project (
project_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_user_id text NOT NULL,
workspace_id text,
name text NOT NULL,
description text,
root_item_id uuid NOT NULL REFERENCES library_item(item_id) ON DELETE RESTRICT,
active_item_id uuid NOT NULL REFERENCES library_item(item_id) ON DELETE RESTRICT,
settings jsonb DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
deleted_at timestamptz,
created_at timestamptz NOT NULL DEFAULT timezone('utc', now()),
updated_at timestamptz NOT NULL DEFAULT timezone('utc', now())
);

CREATE INDEX IF NOT EXISTS idx_canvas_project_owner_updated
ON canvas_project(owner_user_id, updated_at);
CREATE INDEX IF NOT EXISTS idx_canvas_project_root
ON canvas_project(owner_user_id, root_item_id);

CREATE TABLE IF NOT EXISTS canvas_revision (
revision_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES canvas_project(project_id) ON DELETE CASCADE,
item_id uuid NOT NULL REFERENCES library_item(item_id) ON DELETE RESTRICT,
parent_revision_id uuid REFERENCES canvas_revision(revision_id) ON DELETE SET NULL,
operation text NOT NULL DEFAULT 'import',
provider text,
model_id text,
prompt_hash text,
inputs jsonb DEFAULT '[]'::jsonb,
settings jsonb DEFAULT '{}'::jsonb,
trace_id uuid,
created_by_type text NOT NULL DEFAULT 'human',
created_by_id text,
created_at timestamptz NOT NULL DEFAULT timezone('utc', now())
);

CREATE INDEX IF NOT EXISTS idx_canvas_revision_project_created
ON canvas_revision(project_id, created_at);
CREATE UNIQUE INDEX IF NOT EXISTS uq_canvas_revision_project_item
ON canvas_revision(project_id, item_id);

CREATE TABLE IF NOT EXISTS canvas_annotation (
annotation_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES canvas_project(project_id) ON DELETE CASCADE,
revision_id uuid NOT NULL REFERENCES canvas_revision(revision_id) ON DELETE CASCADE,
parent_annotation_id uuid REFERENCES canvas_annotation(annotation_id) ON DELETE CASCADE,
kind text NOT NULL,
body text NOT NULL,
geometry jsonb,
start_ms integer,
end_ms integer,
status text NOT NULL DEFAULT 'open',
author_type text NOT NULL DEFAULT 'human',
author_id text NOT NULL,
metadata jsonb DEFAULT '{}'::jsonb,
deleted_at timestamptz,
created_at timestamptz NOT NULL DEFAULT timezone('utc', now()),
updated_at timestamptz NOT NULL DEFAULT timezone('utc', now()),
CONSTRAINT canvas_annotation_time_range_check CHECK (
(start_ms IS NULL OR start_ms >= 0) AND
(end_ms IS NULL OR end_ms >= coalesce(start_ms, 0))
)
);

CREATE INDEX IF NOT EXISTS idx_canvas_annotation_revision
ON canvas_annotation(project_id, revision_id, created_at);

CREATE TABLE IF NOT EXISTS media_generation_job (
job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid REFERENCES canvas_project(project_id) ON DELETE SET NULL,
owner_user_id text NOT NULL,
workspace_id text,
agent_id text REFERENCES agent(agent_id) ON DELETE SET NULL,
session_id uuid REFERENCES session(session_id) ON DELETE SET NULL,
trace_id uuid,
provider text NOT NULL,
model_id text NOT NULL,
media_kind text NOT NULL,
operation text NOT NULL,
prompt text NOT NULL,
input_item_ids jsonb DEFAULT '[]'::jsonb,
request jsonb DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'queued',
progress integer NOT NULL DEFAULT 0,
provider_operation_id text,
output_item_id uuid REFERENCES library_item(item_id) ON DELETE SET NULL,
error_code text,
error_message text,
estimated_cost_usd real,
actual_cost_usd real,
billing jsonb DEFAULT '{}'::jsonb,
started_at timestamptz,
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT timezone('utc', now()),
updated_at timestamptz NOT NULL DEFAULT timezone('utc', now())
);

CREATE INDEX IF NOT EXISTS idx_media_job_owner_created
ON media_generation_job(owner_user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_media_job_project_created
ON media_generation_job(project_id, created_at);
CREATE INDEX IF NOT EXISTS idx_media_job_queue
ON media_generation_job(status, created_at);

-- Canvas links group existing private Library items without changing access.
ALTER TABLE library_link DROP CONSTRAINT IF EXISTS library_link_scope_type_check;
ALTER TABLE library_link ADD CONSTRAINT library_link_scope_type_check
CHECK (scope_type IN (
'session', 'code_project', 'agent', 'provenance_trace',
'task', 'workflow', 'cli_run', 'sdk_run', 'canvas_project'
));
210 changes: 210 additions & 0 deletions apps/commons-api/models/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,216 @@ export const libraryShareLink = pgTable(
}),
);

/* ──────────────────────── CANVAS MEDIA STUDIO ──────────────────────── */

/**
* A private creative workspace. Canvas does not duplicate media bytes: every
* source and output remains a Library item while this table supplies project
* organization and an active revision pointer.
*/
export const canvasProject = pgTable(
'canvas_project',
{
projectId: uuid('project_id')
.default(sql`gen_random_uuid()`)
.primaryKey(),
ownerUserId: text('owner_user_id').notNull(),
workspaceId: text('workspace_id'),
name: text('name').notNull(),
description: text('description'),
rootItemId: uuid('root_item_id')
.notNull()
.references(() => libraryItem.itemId, { onDelete: 'restrict' }),
activeItemId: uuid('active_item_id')
.notNull()
.references(() => libraryItem.itemId, { onDelete: 'restrict' }),
settings: jsonb('settings').$type<Record<string, unknown>>().default({}),
status: text('status').notNull().default('active'),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true })
.default(sql`timezone('utc', now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.default(sql`timezone('utc', now())`)
.notNull(),
},
(table) => ({
ownerUpdatedIdx: index('idx_canvas_project_owner_updated').on(
table.ownerUserId,
table.updatedAt,
),
rootIdx: index('idx_canvas_project_root').on(
table.ownerUserId,
table.rootItemId,
),
}),
);

/**
* Immutable revision graph. A revision can have many input artifacts in
* `inputs`, while `parentRevisionId` supplies the primary history branch.
*/
export const canvasRevision = pgTable(
'canvas_revision',
{
revisionId: uuid('revision_id')
.default(sql`gen_random_uuid()`)
.primaryKey(),
projectId: uuid('project_id')
.notNull()
.references(() => canvasProject.projectId, { onDelete: 'cascade' }),
itemId: uuid('item_id')
.notNull()
.references(() => libraryItem.itemId, { onDelete: 'restrict' }),
parentRevisionId: uuid('parent_revision_id'),
operation: text('operation').notNull().default('import'),
provider: text('provider'),
modelId: text('model_id'),
promptHash: text('prompt_hash'),
inputs: jsonb('inputs').$type<
Array<{ itemId: string; role?: string; revisionId?: string }>
>().default([]),
settings: jsonb('settings').$type<Record<string, unknown>>().default({}),
// Soft reference because ProvenanceService persists runs asynchronously.
traceId: uuid('trace_id'),
createdByType: text('created_by_type').notNull().default('human'),
createdById: text('created_by_id'),
createdAt: timestamp('created_at', { withTimezone: true })
.default(sql`timezone('utc', now())`)
.notNull(),
},
(table) => ({
projectCreatedIdx: index('idx_canvas_revision_project_created').on(
table.projectId,
table.createdAt,
),
projectItemUnique: uniqueIndex('uq_canvas_revision_project_item').on(
table.projectId,
table.itemId,
),
parentFk: foreignKey({
columns: [table.parentRevisionId],
foreignColumns: [table.revisionId],
name: 'canvas_revision_parent_revision_fk',
}).onDelete('set null'),
}),
);

/**
* Resolution-independent collaboration context. Geometry uses normalized
* coordinates (0..1); temporal ranges use integer milliseconds.
*/
export const canvasAnnotation = pgTable(
'canvas_annotation',
{
annotationId: uuid('annotation_id')
.default(sql`gen_random_uuid()`)
.primaryKey(),
projectId: uuid('project_id')
.notNull()
.references(() => canvasProject.projectId, { onDelete: 'cascade' }),
revisionId: uuid('revision_id')
.notNull()
.references(() => canvasRevision.revisionId, { onDelete: 'cascade' }),
parentAnnotationId: uuid('parent_annotation_id'),
kind: text('kind').notNull(),
body: text('body').notNull(),
geometry: jsonb('geometry').$type<Record<string, unknown>>(),
startMs: integer('start_ms'),
endMs: integer('end_ms'),
status: text('status').notNull().default('open'),
authorType: text('author_type').notNull().default('human'),
authorId: text('author_id').notNull(),
metadata: jsonb('metadata').$type<Record<string, unknown>>().default({}),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true })
.default(sql`timezone('utc', now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.default(sql`timezone('utc', now())`)
.notNull(),
},
(table) => ({
projectRevisionIdx: index('idx_canvas_annotation_revision').on(
table.projectId,
table.revisionId,
table.createdAt,
),
parentFk: foreignKey({
columns: [table.parentAnnotationId],
foreignColumns: [table.annotationId],
name: 'canvas_annotation_parent_annotation_fk',
}).onDelete('cascade'),
timeRangeCheck: check(
'canvas_annotation_time_range_check',
sql`(${table.startMs} is null or ${table.startMs} >= 0) and (${table.endMs} is null or ${table.endMs} >= coalesce(${table.startMs}, 0))`,
),
}),
);

/** Durable queue and audit envelope for user- or agent-initiated media work. */
export const mediaGenerationJob = pgTable(
'media_generation_job',
{
jobId: uuid('job_id')
.default(sql`gen_random_uuid()`)
.primaryKey(),
projectId: uuid('project_id').references(() => canvasProject.projectId, {
onDelete: 'set null',
}),
ownerUserId: text('owner_user_id').notNull(),
workspaceId: text('workspace_id'),
agentId: text('agent_id').references(() => agent.agentId, {
onDelete: 'set null',
}),
sessionId: uuid('session_id').references(() => session.sessionId, {
onDelete: 'set null',
}),
// Soft reference because ProvenanceService persists runs asynchronously.
traceId: uuid('trace_id'),
provider: text('provider').notNull(),
modelId: text('model_id').notNull(),
mediaKind: text('media_kind').notNull(),
operation: text('operation').notNull(),
prompt: text('prompt').notNull(),
inputItemIds: jsonb('input_item_ids').$type<string[]>().default([]),
request: jsonb('request').$type<Record<string, unknown>>().default({}),
status: text('status').notNull().default('queued'),
progress: integer('progress').notNull().default(0),
providerOperationId: text('provider_operation_id'),
outputItemId: uuid('output_item_id').references(() => libraryItem.itemId, {
onDelete: 'set null',
}),
errorCode: text('error_code'),
errorMessage: text('error_message'),
estimatedCostUsd: real('estimated_cost_usd'),
actualCostUsd: real('actual_cost_usd'),
billing: jsonb('billing').$type<Record<string, unknown>>().default({}),
startedAt: timestamp('started_at', { withTimezone: true }),
completedAt: timestamp('completed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true })
.default(sql`timezone('utc', now())`)
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.default(sql`timezone('utc', now())`)
.notNull(),
},
(table) => ({
ownerCreatedIdx: index('idx_media_job_owner_created').on(
table.ownerUserId,
table.createdAt,
),
projectCreatedIdx: index('idx_media_job_project_created').on(
table.projectId,
table.createdAt,
),
queueIdx: index('idx_media_job_queue').on(
table.status,
table.createdAt,
),
}),
);

export const libraryAuditEvent = pgTable(
'library_audit_event',
{
Expand Down
1 change: 1 addition & 0 deletions apps/commons-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"dependencies": {
"@fontsource-variable/space-grotesk": "^5.3.0",
"@google/genai": "^2.20.0",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-select": "^2.2.6",
Expand Down
4 changes: 4 additions & 0 deletions apps/commons-api/src/agent/agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,10 @@ export class AgentService implements OnModuleInit {
- **searchLibraryArtifacts** — on-demand access to the calling user's Library. Omit query to list recent files, or search once by filename, type, or topic. Results are deliberately compact; do not probe with many generic queries. Use the returned fileId with readUploadedFile only when exact contents are needed.
- **readUploadedFile** — read a selected library artifact in bounded chunks after permission checks.
- **generateImage** — create an image with the stable GPT Image API. The result is stored in the owner's artifact library using their storage preference (Private S3 by default).
- **listMediaModels** — inspect exact creative model keys, provider availability, supported controls, and price basis before choosing a model.
- **getCanvasProject** — load a Canvas project's active artifact, revision history, annotations, and generation state before analysing or changing it. Read the active artifact with readUploadedFile when its actual media contents are needed.
- **annotateCanvas** — add precise normalized spatial notes or millisecond time-range notes that users and other agents can inspect and drag into chat.
- **generateMedia** — generate or transform images, video, speech, and music with a listed creative modelKey. Attach input Library item IDs for edits, and pass a Canvas project ID to create a recoverable revision with provenance. Media usage is pre-authorized in Commons credits and settled once from actual provider usage when available.
- **uploadFileToIPFS** — explicit-only public IPFS publishing. Never call this for ordinary uploads, generated files, or library storage unless the user specifically asks for IPFS.

### Uploaded files
Expand Down
Loading
Loading