Skip to content

Commit 1dbf92d

Browse files
fix(api): tool input parsing into table from agent output (#2879)
* fix(api): transformTable to map agent output to table subblock format * fix api * add test
1 parent 3a92364 commit 1dbf92d

File tree

9 files changed

+233
-243
lines changed

9 files changed

+233
-243
lines changed

apps/sim/tools/http/request.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { RequestParams, RequestResponse } from '@/tools/http/types'
2-
import { getDefaultHeaders, processUrl, transformTable } from '@/tools/http/utils'
2+
import { getDefaultHeaders, processUrl } from '@/tools/http/utils'
3+
import { transformTable } from '@/tools/shared/table'
34
import type { ToolConfig } from '@/tools/types'
45

56
export const requestTool: ToolConfig<RequestParams, RequestResponse> = {

apps/sim/tools/http/utils.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { isTest } from '@/lib/core/config/feature-flags'
33
import { getBaseUrl } from '@/lib/core/utils/urls'
4+
import { transformTable } from '@/tools/shared/table'
45
import type { TableRow } from '@/tools/types'
56

67
const logger = createLogger('HTTPRequestUtils')
@@ -119,28 +120,3 @@ export const shouldUseProxy = (url: string): boolean => {
119120
return false
120121
}
121122
}
122-
123-
/**
124-
* Transforms a table from the store format to a key-value object
125-
* Local copy of the function to break circular dependencies
126-
* @param table Array of table rows from the store
127-
* @returns Record of key-value pairs
128-
*/
129-
export const transformTable = (table: TableRow[] | null): Record<string, any> => {
130-
if (!table) return {}
131-
132-
return table.reduce(
133-
(acc, row) => {
134-
if (row.cells?.Key && row.cells?.Value !== undefined) {
135-
// Extract the Value cell as is - it should already be properly resolved
136-
// by the InputResolver based on variable type (number, string, boolean etc.)
137-
const value = row.cells.Value
138-
139-
// Store the correctly typed value in the result object
140-
acc[row.cells.Key] = value
141-
}
142-
return acc
143-
},
144-
{} as Record<string, any>
145-
)
146-
}

apps/sim/tools/knowledge/create_document.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { KnowledgeCreateDocumentResponse } from '@/tools/knowledge/types'
2-
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/params'
32
import { enrichKBTagsSchema } from '@/tools/schema-enrichers'
3+
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/shared/tags'
44
import type { ToolConfig } from '@/tools/types'
55

66
export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumentResponse> = {

apps/sim/tools/knowledge/search.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { KnowledgeSearchResponse } from '@/tools/knowledge/types'
2-
import { parseTagFilters } from '@/tools/params'
32
import { enrichKBTagFiltersSchema } from '@/tools/schema-enrichers'
3+
import { parseTagFilters } from '@/tools/shared/tags'
44
import type { ToolConfig } from '@/tools/types'
55

66
export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {

apps/sim/tools/params.ts

Lines changed: 1 addition & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { createLogger } from '@sim/logger'
2-
import type { StructuredFilter } from '@/lib/knowledge/types'
32
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
43
import {
54
evaluateSubBlockCondition,
65
type SubBlockCondition,
76
} from '@/lib/workflows/subblocks/visibility'
87
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
8+
import { isEmptyTagValue } from '@/tools/shared/tags'
99
import type { ParameterVisibility, ToolConfig } from '@/tools/types'
1010
import { getTool } from '@/tools/utils'
1111

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

26-
/**
27-
* Document tag entry format used in create_document tool
28-
*/
29-
export interface DocumentTagEntry {
30-
tagName: string
31-
value: string
32-
}
33-
34-
/**
35-
* Tag filter entry format used in search tool
36-
*/
37-
export interface TagFilterEntry {
38-
tagName: string
39-
tagSlot?: string
40-
tagValue: string | number | boolean
41-
fieldType?: string
42-
operator?: string
43-
valueTo?: string | number
44-
}
45-
46-
/**
47-
* Checks if a tag value is effectively empty (unfilled/default entry)
48-
*/
49-
function isEmptyTagEntry(entry: Record<string, unknown>): boolean {
50-
if (!entry.tagName || (typeof entry.tagName === 'string' && entry.tagName.trim() === '')) {
51-
return true
52-
}
53-
return false
54-
}
55-
56-
/**
57-
* Checks if a tag-based value is effectively empty (only contains default/unfilled entries).
58-
* Works for both documentTags and tagFilters parameters in various formats.
59-
*
60-
* @param value - The tag value to check (can be JSON string, array, or object)
61-
* @returns true if the value is empty or only contains unfilled entries
62-
*/
63-
export function isEmptyTagValue(value: unknown): boolean {
64-
if (!value) return true
65-
66-
// Handle JSON string format
67-
if (typeof value === 'string') {
68-
try {
69-
const parsed = JSON.parse(value)
70-
if (!Array.isArray(parsed)) return false
71-
if (parsed.length === 0) return true
72-
return parsed.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
73-
} catch {
74-
return false
75-
}
76-
}
77-
78-
// Handle array format directly
79-
if (Array.isArray(value)) {
80-
if (value.length === 0) return true
81-
return value.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
82-
}
83-
84-
// Handle object format (LLM format: { "Category": "foo", "Priority": 5 })
85-
if (typeof value === 'object' && value !== null) {
86-
const entries = Object.entries(value)
87-
if (entries.length === 0) return true
88-
return entries.every(([, val]) => val === undefined || val === null || val === '')
89-
}
90-
91-
return false
92-
}
93-
94-
/**
95-
* Filters valid document tags from an array, removing empty entries
96-
*/
97-
function filterValidDocumentTags(tags: unknown[]): DocumentTagEntry[] {
98-
return tags
99-
.filter((entry): entry is Record<string, unknown> => {
100-
if (typeof entry !== 'object' || entry === null) return false
101-
const e = entry as Record<string, unknown>
102-
if (!e.tagName || (typeof e.tagName === 'string' && e.tagName.trim() === '')) return false
103-
if (e.value === undefined || e.value === null || e.value === '') return false
104-
return true
105-
})
106-
.map((entry) => ({
107-
tagName: String(entry.tagName),
108-
value: String(entry.value),
109-
}))
110-
}
111-
112-
/**
113-
* Parses document tags from various formats into a normalized array format.
114-
* Used by create_document tool to handle tags from both UI and LLM sources.
115-
*
116-
* @param value - Document tags in object, array, or JSON string format
117-
* @returns Normalized array of document tag entries, or empty array if invalid
118-
*/
119-
export function parseDocumentTags(value: unknown): DocumentTagEntry[] {
120-
if (!value) return []
121-
122-
// Handle object format from LLM: { "Category": "foo", "Priority": 5 }
123-
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
124-
return Object.entries(value)
125-
.filter(([tagName, tagValue]) => {
126-
if (!tagName || tagName.trim() === '') return false
127-
if (tagValue === undefined || tagValue === null || tagValue === '') return false
128-
return true
129-
})
130-
.map(([tagName, tagValue]) => ({
131-
tagName,
132-
value: String(tagValue),
133-
}))
134-
}
135-
136-
// Handle JSON string format from UI
137-
if (typeof value === 'string') {
138-
try {
139-
const parsed = JSON.parse(value)
140-
if (Array.isArray(parsed)) {
141-
return filterValidDocumentTags(parsed)
142-
}
143-
} catch {
144-
// Invalid JSON, return empty
145-
}
146-
return []
147-
}
148-
149-
// Handle array format directly
150-
if (Array.isArray(value)) {
151-
return filterValidDocumentTags(value)
152-
}
153-
154-
return []
155-
}
156-
157-
/**
158-
* Parses tag filters from various formats into a normalized StructuredFilter array.
159-
* Used by search tool to handle tag filters from both UI and LLM sources.
160-
*
161-
* @param value - Tag filters in array or JSON string format
162-
* @returns Normalized array of structured filters, or empty array if invalid
163-
*/
164-
export function parseTagFilters(value: unknown): StructuredFilter[] {
165-
if (!value) return []
166-
167-
let tagFilters = value
168-
169-
// Handle JSON string format
170-
if (typeof tagFilters === 'string') {
171-
try {
172-
tagFilters = JSON.parse(tagFilters)
173-
} catch {
174-
return []
175-
}
176-
}
177-
178-
// Must be an array at this point
179-
if (!Array.isArray(tagFilters)) return []
180-
181-
return tagFilters
182-
.filter((filter): filter is Record<string, unknown> => {
183-
if (typeof filter !== 'object' || filter === null) return false
184-
const f = filter as Record<string, unknown>
185-
if (!f.tagName || (typeof f.tagName === 'string' && f.tagName.trim() === '')) return false
186-
if (f.fieldType === 'boolean') {
187-
return f.tagValue !== undefined
188-
}
189-
if (f.tagValue === undefined || f.tagValue === null) return false
190-
if (typeof f.tagValue === 'string' && f.tagValue.trim().length === 0) return false
191-
return true
192-
})
193-
.map((filter) => ({
194-
tagName: filter.tagName as string,
195-
tagSlot: (filter.tagSlot as string) || '',
196-
fieldType: (filter.fieldType as string) || 'text',
197-
operator: (filter.operator as string) || 'eq',
198-
value: filter.tagValue as string | number | boolean,
199-
valueTo: filter.valueTo as string | number | undefined,
200-
}))
201-
}
202-
203-
/**
204-
* Converts parsed document tags to the format expected by the create document API.
205-
* Returns the documentTagsData JSON string if there are valid tags.
206-
*/
207-
export function formatDocumentTagsForAPI(tags: DocumentTagEntry[]): { documentTagsData?: string } {
208-
if (tags.length === 0) return {}
209-
return {
210-
documentTagsData: JSON.stringify(tags),
211-
}
212-
}
213-
21426
export interface Option {
21527
label: string
21628
value: string

apps/sim/tools/shared/table.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { TableRow } from '@/tools/types'
2+
3+
/**
4+
* Transforms a table from the store format to a key-value object.
5+
*/
6+
export const transformTable = (
7+
table: TableRow[] | Record<string, any> | string | null
8+
): Record<string, any> => {
9+
if (!table) return {}
10+
11+
if (typeof table === 'string') {
12+
try {
13+
const parsed = JSON.parse(table) as TableRow[] | Record<string, any>
14+
return transformTable(parsed)
15+
} catch {
16+
return {}
17+
}
18+
}
19+
20+
if (Array.isArray(table)) {
21+
return table.reduce(
22+
(acc, row) => {
23+
if (row.cells?.Key && row.cells?.Value !== undefined) {
24+
const value = row.cells.Value
25+
acc[row.cells.Key] = value
26+
}
27+
return acc
28+
},
29+
{} as Record<string, any>
30+
)
31+
}
32+
33+
if (typeof table === 'object') {
34+
return table
35+
}
36+
37+
return {}
38+
}

0 commit comments

Comments
 (0)