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
3 changes: 2 additions & 1 deletion apps/sim/tools/http/request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RequestParams, RequestResponse } from '@/tools/http/types'
import { getDefaultHeaders, processUrl, transformTable } from '@/tools/http/utils'
import { getDefaultHeaders, processUrl } from '@/tools/http/utils'
import { transformTable } from '@/tools/shared/table'
import type { ToolConfig } from '@/tools/types'

export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
Expand Down
26 changes: 1 addition & 25 deletions apps/sim/tools/http/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { isTest } from '@/lib/core/config/feature-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { transformTable } from '@/tools/shared/table'
import type { TableRow } from '@/tools/types'

const logger = createLogger('HTTPRequestUtils')
Expand Down Expand Up @@ -119,28 +120,3 @@ export const shouldUseProxy = (url: string): boolean => {
return false
}
}

/**
* Transforms a table from the store format to a key-value object
* Local copy of the function to break circular dependencies
* @param table Array of table rows from the store
* @returns Record of key-value pairs
*/
export const transformTable = (table: TableRow[] | null): Record<string, any> => {
if (!table) return {}

return table.reduce(
(acc, row) => {
if (row.cells?.Key && row.cells?.Value !== undefined) {
// Extract the Value cell as is - it should already be properly resolved
// by the InputResolver based on variable type (number, string, boolean etc.)
const value = row.cells.Value

// Store the correctly typed value in the result object
acc[row.cells.Key] = value
}
return acc
},
{} as Record<string, any>
)
}
2 changes: 1 addition & 1 deletion apps/sim/tools/knowledge/create_document.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { KnowledgeCreateDocumentResponse } from '@/tools/knowledge/types'
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/params'
import { enrichKBTagsSchema } from '@/tools/schema-enrichers'
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/shared/tags'
import type { ToolConfig } from '@/tools/types'

export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumentResponse> = {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/tools/knowledge/search.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { KnowledgeSearchResponse } from '@/tools/knowledge/types'
import { parseTagFilters } from '@/tools/params'
import { enrichKBTagFiltersSchema } from '@/tools/schema-enrichers'
import { parseTagFilters } from '@/tools/shared/tags'
import type { ToolConfig } from '@/tools/types'

export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
Expand Down
190 changes: 1 addition & 189 deletions apps/sim/tools/params.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createLogger } from '@sim/logger'
import type { StructuredFilter } from '@/lib/knowledge/types'
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
import {
evaluateSubBlockCondition,
type SubBlockCondition,
} from '@/lib/workflows/subblocks/visibility'
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
import { isEmptyTagValue } from '@/tools/shared/tags'
import type { ParameterVisibility, ToolConfig } from '@/tools/types'
import { getTool } from '@/tools/utils'

Expand All @@ -23,194 +23,6 @@ export function isNonEmpty(value: unknown): boolean {
// Tag/Value Parsing Utilities
// ============================================================================

/**
* Document tag entry format used in create_document tool
*/
export interface DocumentTagEntry {
tagName: string
value: string
}

/**
* Tag filter entry format used in search tool
*/
export interface TagFilterEntry {
tagName: string
tagSlot?: string
tagValue: string | number | boolean
fieldType?: string
operator?: string
valueTo?: string | number
}

/**
* Checks if a tag value is effectively empty (unfilled/default entry)
*/
function isEmptyTagEntry(entry: Record<string, unknown>): boolean {
if (!entry.tagName || (typeof entry.tagName === 'string' && entry.tagName.trim() === '')) {
return true
}
return false
}

/**
* Checks if a tag-based value is effectively empty (only contains default/unfilled entries).
* Works for both documentTags and tagFilters parameters in various formats.
*
* @param value - The tag value to check (can be JSON string, array, or object)
* @returns true if the value is empty or only contains unfilled entries
*/
export function isEmptyTagValue(value: unknown): boolean {
if (!value) return true

// Handle JSON string format
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
if (!Array.isArray(parsed)) return false
if (parsed.length === 0) return true
return parsed.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
} catch {
return false
}
}

// Handle array format directly
if (Array.isArray(value)) {
if (value.length === 0) return true
return value.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
}

// Handle object format (LLM format: { "Category": "foo", "Priority": 5 })
if (typeof value === 'object' && value !== null) {
const entries = Object.entries(value)
if (entries.length === 0) return true
return entries.every(([, val]) => val === undefined || val === null || val === '')
}

return false
}

/**
* Filters valid document tags from an array, removing empty entries
*/
function filterValidDocumentTags(tags: unknown[]): DocumentTagEntry[] {
return tags
.filter((entry): entry is Record<string, unknown> => {
if (typeof entry !== 'object' || entry === null) return false
const e = entry as Record<string, unknown>
if (!e.tagName || (typeof e.tagName === 'string' && e.tagName.trim() === '')) return false
if (e.value === undefined || e.value === null || e.value === '') return false
return true
})
.map((entry) => ({
tagName: String(entry.tagName),
value: String(entry.value),
}))
}

/**
* Parses document tags from various formats into a normalized array format.
* Used by create_document tool to handle tags from both UI and LLM sources.
*
* @param value - Document tags in object, array, or JSON string format
* @returns Normalized array of document tag entries, or empty array if invalid
*/
export function parseDocumentTags(value: unknown): DocumentTagEntry[] {
if (!value) return []

// Handle object format from LLM: { "Category": "foo", "Priority": 5 }
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
return Object.entries(value)
.filter(([tagName, tagValue]) => {
if (!tagName || tagName.trim() === '') return false
if (tagValue === undefined || tagValue === null || tagValue === '') return false
return true
})
.map(([tagName, tagValue]) => ({
tagName,
value: String(tagValue),
}))
}

// Handle JSON string format from UI
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
if (Array.isArray(parsed)) {
return filterValidDocumentTags(parsed)
}
} catch {
// Invalid JSON, return empty
}
return []
}

// Handle array format directly
if (Array.isArray(value)) {
return filterValidDocumentTags(value)
}

return []
}

/**
* Parses tag filters from various formats into a normalized StructuredFilter array.
* Used by search tool to handle tag filters from both UI and LLM sources.
*
* @param value - Tag filters in array or JSON string format
* @returns Normalized array of structured filters, or empty array if invalid
*/
export function parseTagFilters(value: unknown): StructuredFilter[] {
if (!value) return []

let tagFilters = value

// Handle JSON string format
if (typeof tagFilters === 'string') {
try {
tagFilters = JSON.parse(tagFilters)
} catch {
return []
}
}

// Must be an array at this point
if (!Array.isArray(tagFilters)) return []

return tagFilters
.filter((filter): filter is Record<string, unknown> => {
if (typeof filter !== 'object' || filter === null) return false
const f = filter as Record<string, unknown>
if (!f.tagName || (typeof f.tagName === 'string' && f.tagName.trim() === '')) return false
if (f.fieldType === 'boolean') {
return f.tagValue !== undefined
}
if (f.tagValue === undefined || f.tagValue === null) return false
if (typeof f.tagValue === 'string' && f.tagValue.trim().length === 0) return false
return true
})
.map((filter) => ({
tagName: filter.tagName as string,
tagSlot: (filter.tagSlot as string) || '',
fieldType: (filter.fieldType as string) || 'text',
operator: (filter.operator as string) || 'eq',
value: filter.tagValue as string | number | boolean,
valueTo: filter.valueTo as string | number | undefined,
}))
}

/**
* Converts parsed document tags to the format expected by the create document API.
* Returns the documentTagsData JSON string if there are valid tags.
*/
export function formatDocumentTagsForAPI(tags: DocumentTagEntry[]): { documentTagsData?: string } {
if (tags.length === 0) return {}
return {
documentTagsData: JSON.stringify(tags),
}
}

export interface Option {
label: string
value: string
Expand Down
38 changes: 38 additions & 0 deletions apps/sim/tools/shared/table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { TableRow } from '@/tools/types'

/**
* Transforms a table from the store format to a key-value object.
*/
export const transformTable = (
table: TableRow[] | Record<string, any> | string | null
): Record<string, any> => {
if (!table) return {}

if (typeof table === 'string') {
try {
const parsed = JSON.parse(table) as TableRow[] | Record<string, any>
return transformTable(parsed)
} catch {
return {}
}
}

if (Array.isArray(table)) {
return table.reduce(
(acc, row) => {
if (row.cells?.Key && row.cells?.Value !== undefined) {
const value = row.cells.Value
acc[row.cells.Key] = value
}
return acc
},
{} as Record<string, any>
)
}

if (typeof table === 'object') {
return table
}

return {}
}
Loading