d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
193 lines
5.0 KiB
TypeScript
193 lines
5.0 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { toError } from '@sim/utils/errors'
|
|
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
|
|
|
const logger = createLogger('CustomToolsPersistence')
|
|
|
|
interface CustomTool {
|
|
id?: string
|
|
type: 'custom-tool'
|
|
title: string
|
|
toolId?: string
|
|
schema: {
|
|
function: {
|
|
name?: string
|
|
description: string
|
|
parameters: Record<string, any>
|
|
}
|
|
}
|
|
code: string
|
|
usageControl?: string
|
|
}
|
|
|
|
/**
|
|
* Stored tool format that may contain either reference or inline definition
|
|
*/
|
|
interface StoredCustomTool {
|
|
type: string
|
|
title?: string
|
|
toolId?: string
|
|
customToolId?: string
|
|
schema?: any
|
|
code?: string
|
|
usageControl?: string
|
|
}
|
|
|
|
/**
|
|
* Checks if a stored tool is a reference-only custom tool (no inline definition)
|
|
*/
|
|
function isCustomToolReference(tool: StoredCustomTool): boolean {
|
|
return tool.type === 'custom-tool' && !!tool.customToolId && !tool.code
|
|
}
|
|
|
|
/**
|
|
* Extract all custom tools from agent blocks in the workflow state
|
|
*
|
|
* @remarks
|
|
* Only extracts tools with inline definitions (legacy format).
|
|
* Reference-only tools (new format with customToolId) are skipped since they're already in the database.
|
|
*/
|
|
export function extractCustomToolsFromWorkflowState(workflowState: any): CustomTool[] {
|
|
const customToolsMap = new Map<string, CustomTool>()
|
|
|
|
if (!workflowState?.blocks) {
|
|
return []
|
|
}
|
|
|
|
for (const [blockId, block] of Object.entries(workflowState.blocks)) {
|
|
try {
|
|
const blockData = block as any
|
|
|
|
if (!blockData || blockData.type !== 'agent') {
|
|
continue
|
|
}
|
|
|
|
const subBlocks = blockData.subBlocks || {}
|
|
const toolsSubBlock = subBlocks.tools
|
|
|
|
if (!toolsSubBlock?.value) {
|
|
continue
|
|
}
|
|
|
|
let tools = toolsSubBlock.value
|
|
|
|
if (typeof tools === 'string') {
|
|
try {
|
|
tools = JSON.parse(tools)
|
|
} catch (error) {
|
|
logger.warn(`Failed to parse tools in block ${blockId}`, { error })
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(tools)) {
|
|
continue
|
|
}
|
|
|
|
for (const tool of tools) {
|
|
if (!tool || typeof tool !== 'object' || tool.type !== 'custom-tool') {
|
|
continue
|
|
}
|
|
|
|
// Skip reference-only tools - they're already in the database
|
|
if (isCustomToolReference(tool)) {
|
|
logger.debug(`Skipping reference-only custom tool: ${tool.title || tool.customToolId}`)
|
|
continue
|
|
}
|
|
|
|
// Only persist tools with inline definitions (legacy format)
|
|
if (tool.title && tool.schema?.function && tool.code) {
|
|
const toolKey = tool.toolId || tool.title
|
|
|
|
if (!customToolsMap.has(toolKey)) {
|
|
customToolsMap.set(toolKey, tool as CustomTool)
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Error extracting custom tools from block ${blockId}`, { error })
|
|
}
|
|
}
|
|
|
|
return Array.from(customToolsMap.values())
|
|
}
|
|
|
|
/**
|
|
* Persist custom tools to the database using the upsert function
|
|
* Creates new tools or updates existing ones
|
|
*/
|
|
export async function persistCustomToolsToDatabase(
|
|
customToolsList: CustomTool[],
|
|
workspaceId: string | null,
|
|
userId: string
|
|
): Promise<{ saved: number; errors: string[] }> {
|
|
if (!customToolsList || customToolsList.length === 0) {
|
|
return { saved: 0, errors: [] }
|
|
}
|
|
|
|
if (!workspaceId) {
|
|
logger.debug('Skipping custom tools persistence - no workspaceId provided (user-scoped tools)')
|
|
return { saved: 0, errors: [] }
|
|
}
|
|
|
|
const errors: string[] = []
|
|
let saved = 0
|
|
|
|
const validTools = customToolsList.filter((tool) => {
|
|
if (!tool.schema?.function?.name) {
|
|
logger.warn(`Skipping custom tool without function name: ${tool.title}`)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
if (validTools.length === 0) {
|
|
return { saved: 0, errors: [] }
|
|
}
|
|
|
|
try {
|
|
await upsertCustomTools({
|
|
tools: validTools.map((tool) => ({
|
|
id: tool.toolId,
|
|
title: tool.title,
|
|
schema: tool.schema,
|
|
code: tool.code,
|
|
})),
|
|
workspaceId,
|
|
userId,
|
|
})
|
|
|
|
saved = validTools.length
|
|
logger.info(`Persisted ${saved} custom tool(s)`, { workspaceId })
|
|
} catch (error) {
|
|
const errorMsg = `Failed to persist custom tools: ${toError(error).message}`
|
|
logger.error(errorMsg, { error })
|
|
errors.push(errorMsg)
|
|
}
|
|
|
|
return { saved, errors }
|
|
}
|
|
|
|
/**
|
|
* Extract and persist custom tools from workflow state
|
|
*/
|
|
export async function extractAndPersistCustomTools(
|
|
workflowState: any,
|
|
workspaceId: string | null,
|
|
userId: string
|
|
): Promise<{ saved: number; errors: string[] }> {
|
|
const customToolsList = extractCustomToolsFromWorkflowState(workflowState)
|
|
|
|
if (customToolsList.length === 0) {
|
|
logger.debug('No custom tools found in workflow state')
|
|
return { saved: 0, errors: [] }
|
|
}
|
|
|
|
logger.info(`Found ${customToolsList.length} custom tool(s) to persist`, {
|
|
tools: customToolsList.map((t) => t.title),
|
|
workspaceId,
|
|
})
|
|
|
|
return await persistCustomToolsToDatabase(customToolsList, workspaceId, userId)
|
|
}
|