Skip to content

Commit bcdfc85

Browse files
committed
Tool updates
1 parent e921448 commit bcdfc85

File tree

10 files changed

+327
-47
lines changed

10 files changed

+327
-47
lines changed

apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import crypto from 'crypto'
22
import { db } from '@sim/db'
3-
import { chat, workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
4-
import { eq, inArray } from 'drizzle-orm'
3+
import {
4+
chat,
5+
workflow,
6+
workflowDeploymentVersion,
7+
workflowMcpServer,
8+
workflowMcpTool,
9+
} from '@sim/db/schema'
10+
import { and, eq, inArray } from 'drizzle-orm'
511
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types'
612
import { mcpPubSub } from '@/lib/mcp/pubsub'
713
import { generateParameterSchemaForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
@@ -313,3 +319,87 @@ export async function executeDeleteWorkspaceMcpServer(
313319
return { success: false, error: error instanceof Error ? error.message : String(error) }
314320
}
315321
}
322+
323+
export async function executeGetDeploymentVersion(
324+
params: { workflowId?: string; version?: number },
325+
context: ExecutionContext
326+
): Promise<ToolCallResult> {
327+
try {
328+
const workflowId = params.workflowId || context.workflowId
329+
if (!workflowId) {
330+
return { success: false, error: 'workflowId is required' }
331+
}
332+
const version = params.version
333+
if (version === undefined || version === null) {
334+
return { success: false, error: 'version is required' }
335+
}
336+
337+
await ensureWorkflowAccess(workflowId, context.userId)
338+
339+
const [row] = await db
340+
.select({ state: workflowDeploymentVersion.state })
341+
.from(workflowDeploymentVersion)
342+
.where(
343+
and(
344+
eq(workflowDeploymentVersion.workflowId, workflowId),
345+
eq(workflowDeploymentVersion.version, version)
346+
)
347+
)
348+
.limit(1)
349+
350+
if (!row?.state) {
351+
return { success: false, error: `Deployment version ${version} not found` }
352+
}
353+
354+
return { success: true, output: { version, deployedState: row.state } }
355+
} catch (error) {
356+
return { success: false, error: error instanceof Error ? error.message : String(error) }
357+
}
358+
}
359+
360+
export async function executeRevertToVersion(
361+
params: { workflowId?: string; version?: number },
362+
context: ExecutionContext
363+
): Promise<ToolCallResult> {
364+
try {
365+
const workflowId = params.workflowId || context.workflowId
366+
if (!workflowId) {
367+
return { success: false, error: 'workflowId is required' }
368+
}
369+
const version = params.version
370+
if (version === undefined || version === null) {
371+
return { success: false, error: 'version is required' }
372+
}
373+
374+
await ensureWorkflowAccess(workflowId, context.userId)
375+
376+
const baseUrl =
377+
process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || 'http://localhost:3000'
378+
const response = await fetch(
379+
`${baseUrl}/api/workflows/${workflowId}/deployments/${version}/revert`,
380+
{
381+
method: 'POST',
382+
headers: {
383+
'Content-Type': 'application/json',
384+
'x-api-key': process.env.INTERNAL_API_SECRET || '',
385+
},
386+
}
387+
)
388+
389+
if (!response.ok) {
390+
const body = await response.json().catch(() => ({}))
391+
return { success: false, error: body.error || `Failed to revert (HTTP ${response.status})` }
392+
}
393+
394+
const result = await response.json()
395+
return {
396+
success: true,
397+
output: {
398+
message: `Reverted workflow to deployment version ${version}`,
399+
lastSaved: result.lastSaved,
400+
},
401+
}
402+
} catch (error) {
403+
return { success: false, error: error instanceof Error ? error.message : String(error) }
404+
}
405+
}

apps/sim/lib/copilot/orchestrator/tool-executor/index.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@sim/db'
2-
import { mcpServers, pendingCredentialDraft, user } from '@sim/db/schema'
2+
import { credential, mcpServers, pendingCredentialDraft, user } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, eq, isNull, lt } from 'drizzle-orm'
55
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
@@ -34,8 +34,10 @@ import {
3434
executeDeployApi,
3535
executeDeployChat,
3636
executeDeployMcp,
37+
executeGetDeploymentVersion,
3738
executeListWorkspaceMcpServers,
3839
executeRedeploy,
40+
executeRevertToVersion,
3941
executeUpdateWorkspaceMcpServer,
4042
} from './deployment-tools'
4143
import { executeIntegrationToolDirect } from './integration-tools'
@@ -876,6 +878,57 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
876878
executeUpdateWorkspaceMcpServer(p as unknown as UpdateWorkspaceMcpServerParams, c),
877879
delete_workspace_mcp_server: (p, c) =>
878880
executeDeleteWorkspaceMcpServer(p as unknown as DeleteWorkspaceMcpServerParams, c),
881+
get_deployment_version: (p, c) =>
882+
executeGetDeploymentVersion(p as { workflowId?: string; version?: number }, c),
883+
revert_to_version: (p, c) =>
884+
executeRevertToVersion(p as { workflowId?: string; version?: number }, c),
885+
manage_credential: async (p, c) => {
886+
const params = p as { operation: string; credentialId: string; displayName?: string }
887+
const { operation, credentialId, displayName } = params
888+
if (!credentialId) {
889+
return { success: false, error: 'credentialId is required' }
890+
}
891+
try {
892+
const [row] = await db
893+
.select({ id: credential.id, type: credential.type, displayName: credential.displayName })
894+
.from(credential)
895+
.where(eq(credential.id, credentialId))
896+
.limit(1)
897+
if (!row) {
898+
return { success: false, error: 'Credential not found' }
899+
}
900+
if (row.type !== 'oauth') {
901+
return {
902+
success: false,
903+
error:
904+
'Only OAuth credentials can be managed with this tool. Use set_environment_variables for env vars.',
905+
}
906+
}
907+
switch (operation) {
908+
case 'rename': {
909+
if (!displayName) {
910+
return { success: false, error: 'displayName is required for rename' }
911+
}
912+
await db
913+
.update(credential)
914+
.set({ displayName, updatedAt: new Date() })
915+
.where(eq(credential.id, credentialId))
916+
return { success: true, output: { credentialId, displayName } }
917+
}
918+
case 'delete': {
919+
await db.delete(credential).where(eq(credential.id, credentialId))
920+
return { success: true, output: { credentialId, deleted: true } }
921+
}
922+
default:
923+
return {
924+
success: false,
925+
error: `Unknown operation: ${operation}. Use "rename" or "delete".`,
926+
}
927+
}
928+
} catch (error) {
929+
return { success: false, error: error instanceof Error ? error.message : String(error) }
930+
}
931+
},
879932
create_job: (p, c) => executeCreateJob(p, c),
880933
manage_job: (p, c) => executeManageJob(p, c),
881934
complete_job: (p, c) => executeCompleteJob(p, c),

apps/sim/lib/copilot/orchestrator/tool-executor/job-tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ export async function executeManageJob(
352352
if (!['active', 'paused'].includes(args.status)) {
353353
return { success: false, error: 'status must be "active" or "paused"' }
354354
}
355-
updates.status = args.status
355+
updates.status = args.status === 'paused' ? 'disabled' : args.status
356356
}
357357

358358
if (args.cron !== undefined) {

apps/sim/lib/copilot/tools/mcp/definitions.ts

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,6 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [
3737
},
3838
annotations: { readOnlyHint: true },
3939
},
40-
{
41-
name: 'list_workflows',
42-
toolId: 'list_user_workflows',
43-
description:
44-
'List all workflows the user has access to. Returns workflow IDs, names, workspace, and folder info. Use workspaceId/folderId to scope results.',
45-
inputSchema: {
46-
type: 'object',
47-
properties: {
48-
workspaceId: {
49-
type: 'string',
50-
description: 'Optional workspace ID to filter workflows.',
51-
},
52-
folderId: {
53-
type: 'string',
54-
description: 'Optional folder ID to filter workflows.',
55-
},
56-
},
57-
},
58-
annotations: { readOnlyHint: true },
59-
},
6040
{
6141
name: 'list_folders',
6242
toolId: 'list_folders',
@@ -74,23 +54,6 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [
7454
},
7555
annotations: { readOnlyHint: true },
7656
},
77-
{
78-
name: 'get_workflow',
79-
toolId: 'get_user_workflow',
80-
description:
81-
'Get a workflow by ID. Returns the full workflow definition including all blocks, connections, and configuration.',
82-
inputSchema: {
83-
type: 'object',
84-
properties: {
85-
workflowId: {
86-
type: 'string',
87-
description: 'Workflow ID to retrieve.',
88-
},
89-
},
90-
required: ['workflowId'],
91-
},
92-
annotations: { readOnlyHint: true },
93-
},
9457
{
9558
name: 'create_workflow',
9659
toolId: 'create_workflow',

apps/sim/lib/copilot/tools/server/files/workspace-file.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,26 @@ import type { WorkspaceFileArgs, WorkspaceFileResult } from '@/lib/copilot/tools
44
import {
55
deleteWorkspaceFile,
66
getWorkspaceFile,
7+
updateWorkspaceFileContent,
78
uploadWorkspaceFile,
89
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
910

1011
const logger = createLogger('WorkspaceFileServerTool')
1112

13+
const EXT_TO_MIME: Record<string, string> = {
14+
'.txt': 'text/plain',
15+
'.md': 'text/markdown',
16+
'.html': 'text/html',
17+
'.json': 'application/json',
18+
'.csv': 'text/csv',
19+
}
20+
21+
function inferContentType(fileName: string, explicitType?: string): string {
22+
if (explicitType) return explicitType
23+
const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase()
24+
return EXT_TO_MIME[ext] || 'text/plain'
25+
}
26+
1227
export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, WorkspaceFileResult> = {
1328
name: 'workspace_file',
1429
async execute(
@@ -33,8 +48,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
3348
case 'write': {
3449
const fileName = (args as Record<string, unknown>).fileName as string | undefined
3550
const content = (args as Record<string, unknown>).content as string | undefined
36-
const contentType =
37-
((args as Record<string, unknown>).contentType as string) || 'text/plain'
51+
const explicitType = (args as Record<string, unknown>).contentType as string | undefined
3852

3953
if (!fileName) {
4054
return { success: false, message: 'fileName is required for write operation' }
@@ -43,6 +57,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
4357
return { success: false, message: 'content is required for write operation' }
4458
}
4559

60+
const contentType = inferContentType(fileName, explicitType)
4661
const fileBuffer = Buffer.from(content, 'utf-8')
4762
const result = await uploadWorkspaceFile(
4863
workspaceId,
@@ -72,6 +87,43 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
7287
}
7388
}
7489

90+
case 'update': {
91+
const fileId = (args as Record<string, unknown>).fileId as string | undefined
92+
const content = (args as Record<string, unknown>).content as string | undefined
93+
94+
if (!fileId) {
95+
return { success: false, message: 'fileId is required for update operation' }
96+
}
97+
if (content === undefined || content === null) {
98+
return { success: false, message: 'content is required for update operation' }
99+
}
100+
101+
const fileRecord = await getWorkspaceFile(workspaceId, fileId)
102+
if (!fileRecord) {
103+
return { success: false, message: `File with ID "${fileId}" not found` }
104+
}
105+
106+
const fileBuffer = Buffer.from(content, 'utf-8')
107+
await updateWorkspaceFileContent(workspaceId, fileId, context.userId, fileBuffer)
108+
109+
logger.info('Workspace file updated via copilot', {
110+
fileId,
111+
name: fileRecord.name,
112+
size: fileBuffer.length,
113+
userId: context.userId,
114+
})
115+
116+
return {
117+
success: true,
118+
message: `File "${fileRecord.name}" updated successfully (${fileBuffer.length} bytes)`,
119+
data: {
120+
id: fileId,
121+
name: fileRecord.name,
122+
size: fileBuffer.length,
123+
},
124+
}
125+
}
126+
75127
case 'delete': {
76128
const fileId = (args as Record<string, unknown>).fileId as string | undefined
77129
if (!fileId) {

apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { generateInternalToken } from '@/lib/auth/internal'
66
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
77
import type { KnowledgeBaseArgs, KnowledgeBaseResult } from '@/lib/copilot/tools/shared/schemas'
88
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
9-
import { createSingleDocument, processDocumentAsync } from '@/lib/knowledge/documents/service'
9+
import {
10+
createSingleDocument,
11+
deleteDocument,
12+
processDocumentAsync,
13+
updateDocument,
14+
} from '@/lib/knowledge/documents/service'
1015
import { generateSearchEmbedding } from '@/lib/knowledge/embeddings'
1116
import {
1217
createKnowledgeBase,
@@ -381,6 +386,55 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
381386
}
382387
}
383388

389+
case 'delete_document': {
390+
if (!args.knowledgeBaseId) {
391+
return { success: false, message: 'knowledgeBaseId is required for delete_document' }
392+
}
393+
if (!args.documentId) {
394+
return { success: false, message: 'documentId is required for delete_document' }
395+
}
396+
const requestId = crypto.randomUUID().slice(0, 8)
397+
const result = await deleteDocument(args.documentId, requestId)
398+
return {
399+
success: result.success,
400+
message: result.message,
401+
data: { documentId: args.documentId, knowledgeBaseId: args.knowledgeBaseId },
402+
}
403+
}
404+
405+
case 'update_document': {
406+
if (!args.knowledgeBaseId) {
407+
return { success: false, message: 'knowledgeBaseId is required for update_document' }
408+
}
409+
if (!args.documentId) {
410+
return { success: false, message: 'documentId is required for update_document' }
411+
}
412+
const updateData: { filename?: string; enabled?: boolean } = {}
413+
if (args.filename !== undefined) {
414+
updateData.filename = args.filename
415+
}
416+
if (args.enabled !== undefined) {
417+
updateData.enabled = args.enabled
418+
}
419+
if (Object.keys(updateData).length === 0) {
420+
return {
421+
success: false,
422+
message: 'At least one of filename or enabled is required for update_document',
423+
}
424+
}
425+
const requestId = crypto.randomUUID().slice(0, 8)
426+
await updateDocument(args.documentId, updateData, requestId)
427+
return {
428+
success: true,
429+
message: `Document updated successfully`,
430+
data: {
431+
documentId: args.documentId,
432+
knowledgeBaseId: args.knowledgeBaseId,
433+
...updateData,
434+
},
435+
}
436+
}
437+
384438
case 'list_tags': {
385439
if (!args.knowledgeBaseId) {
386440
return {

0 commit comments

Comments
 (0)