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
7 changes: 4 additions & 3 deletions apps/sim/blocks/blocks/hubspot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { HubspotIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { toolCallArg } from '@/blocks/utils'
import { getTrigger } from '@/triggers'

export const HubSpotBlock: BlockConfig = {
Expand Down Expand Up @@ -1631,7 +1632,7 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
throw new Error(`Unknown operation: ${params.operation}`)
}
},
params: (params) => {
params: (params, context) => {
const {
oauthCredential,
operation,
Expand Down Expand Up @@ -1698,8 +1699,8 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
'search_line_items',
'search_quotes',
]
if (searchProperties && searchOps.includes(operation as string)) {
cleanParams.properties = searchProperties
if (searchOps.includes(operation as string)) {
cleanParams.properties = searchProperties || toolCallArg(context, properties)
}

if (filterGroups && searchOps.includes(operation as string)) {
Expand Down
72 changes: 72 additions & 0 deletions apps/sim/blocks/blocks/servicenow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,75 @@ describe('ServiceNow state written after the split is never re-migrated', () =>
expect(second.blocks['block-1'].subBlocks.readFields?.value).toBe('number,state')
})
})

/**
* A tool call (agent, Pi, Human v2) passes the model's args under the tool param
* names. Operations whose value has a dedicated, differently-named subblock must
* fall back to the model's arg instead of overwriting it with the blank subblock.
*/
describe('ServiceNow tool-call args reach the tool', () => {
function mapToolCall(operation: string, args: Record<string, unknown>): Record<string, unknown> {
const transform = ServiceNowBlock.tools.config?.params
if (!transform) throw new Error('ServiceNow block has no params transform')
const params = { ...CREDENTIALS, ...args }
return { ...params, ...transform(params, { operation }) }
}

it('keeps the model close code and notes on Resolve Incident', () => {
const mapped = mapToolCall('servicenow_resolve_incident', {
sysId: 'abc',
closeCode: 'Solution provided',
closeNotes: 'Restarted the service',
})

expect(mapped.closeCode).toBe('Solution provided')
expect(mapped.closeNotes).toBe('Restarted the service')
})

it('keeps the model state filter, projection, and display mode on List Incidents', () => {
const mapped = mapToolCall('servicenow_list_incidents', {
state: '2',
fields: 'number,state',
displayValue: 'false',
})

expect(mapped.state).toBe('2')
expect(mapped.fields).toBe('number,state')
expect(mapped.displayValue).toBe('false')
})

it('does not send a stored Create/Update JSON body as the List Incidents projection', () => {
const mapped = mapToolCall('servicenow_list_incidents', {
fields: '{"short_description":"Printer down"}',
})

expect(mapped.fields).toBeUndefined()
})

it('keeps the model comment on Update Approval and the search text on Search Knowledge', () => {
expect(
mapToolCall('servicenow_update_approval', {
approvalSysId: 'abc',
decision: 'approved',
comments: 'Looks good',
}).comments
).toBe('Looks good')
expect(mapToolCall('servicenow_search_knowledge', { query: 'vpn' }).query).toBe('vpn')
})

it('keeps the model limit on List Attachments', () => {
const mapped = mapToolCall('servicenow_list_attachments', {
tableName: 'incident',
recordSysId: 'abc',
limit: 5,
})

expect(mapped.limit).toBe(5)
})

it('prefers a configured subblock value over the model arg', () => {
const mapped = mapToolCall('servicenow_list_incidents', { incidentState: '6', state: '2' })

expect(mapped.state).toBe('6')
})
})
61 changes: 46 additions & 15 deletions apps/sim/blocks/blocks/servicenow.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ServiceNowIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import type { BlockConfig, BlockMeta, BlockToolParamsContext } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { normalizeFileInput, withSavedCredential } from '@/blocks/utils'
import { isToolCall, normalizeFileInput, toolCallArg, withSavedCredential } from '@/blocks/utils'
import {
APPROVAL_DECISION_OPTIONS,
APPROVAL_STATE,
Expand Down Expand Up @@ -1622,9 +1622,9 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
],
config: {
tool: (params) => params.operation,
params: (params) => {
params: (params, context?: BlockToolParamsContext) => {
const {
operation,
operation: storedOperation,
fields,
readFields,
returnFields,
Expand All @@ -1644,11 +1644,23 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
knowledgeQuery,
...rest
} = params
const operation = context?.operation ?? storedOperation
/**
* A dedicated subblock's value, or on a tool call where it is blank, the
* model's arg under the tool param name. Without a model arg the subblock
* value stands exactly, so an explicit "Any" (`''`) is not replaced.
*/
const dedicatedOrToolArg = (dedicated: unknown, toolArg: unknown) =>
isToolCall(context) &&
toolArg !== undefined &&
(dedicated === undefined || dedicated === null || dedicated === '')
? toolArg
: dedicated
const isCreateOrUpdate =
operation === 'servicenow_create_record' || operation === 'servicenow_update_record'

if (SEMANTIC_DISPLAY_VALUE_OPS.has(operation)) {
rest.displayValue = semanticDisplayValue
rest.displayValue = dedicatedOrToolArg(semanticDisplayValue, rest.displayValue)
}

/**
Expand All @@ -1659,7 +1671,9 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
* query — so each gets its own subblock and is republished here for the
* operations that own it. Assign explicitly rather than conditionally:
* `finalInputs` merges this result over the raw inputs, so a key left
* off is not dropped, it keeps whatever the raw input held.
* off is not dropped, it keeps whatever the raw input held. On a tool
* call the model supplies these under the tool param names, so a blank
* dedicated subblock falls back to the model's value instead of wiping it.
*/
const stateSource =
operation === 'servicenow_update_change_state'
Expand All @@ -1669,12 +1683,24 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
: CHANGE_STATE_OPS.has(operation)
? changeState
: incidentState
rest.state = SEMANTIC_STATE_OPS.has(operation) ? stateSource : undefined
rest.comments =
operation === 'servicenow_update_approval' ? approvalComments : incidentComments
rest.closeCode = CHANGE_CLOSE_OPS.has(operation) ? changeCloseCode : resolutionCode
rest.closeNotes = CHANGE_CLOSE_OPS.has(operation) ? changeCloseNotes : resolutionNotes
if (operation === 'servicenow_search_knowledge') rest.query = knowledgeQuery
rest.state = SEMANTIC_STATE_OPS.has(operation)
? dedicatedOrToolArg(stateSource, rest.state)
: undefined
rest.comments = dedicatedOrToolArg(
operation === 'servicenow_update_approval' ? approvalComments : incidentComments,
rest.comments
)
rest.closeCode = dedicatedOrToolArg(
CHANGE_CLOSE_OPS.has(operation) ? changeCloseCode : resolutionCode,
rest.closeCode
)
rest.closeNotes = dedicatedOrToolArg(
CHANGE_CLOSE_OPS.has(operation) ? changeCloseNotes : resolutionNotes,
rest.closeNotes
)
if (operation === 'servicenow_search_knowledge') {
rest.query = dedicatedOrToolArg(knowledgeQuery, rest.query)
}

/**
* `attachmentLimit` exists as a separate subblock precisely so a limit
Expand All @@ -1684,8 +1710,8 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
* Every paginated operation reads exactly one of the two.
*/
if (operation === 'servicenow_list_attachments') {
rest.limit =
attachmentLimit != null && attachmentLimit !== '' ? Number(attachmentLimit) : undefined
const limit = dedicatedOrToolArg(attachmentLimit, rest.limit)
rest.limit = limit != null && limit !== '' ? Number(limit) : undefined
} else if (rest.limit != null) {
rest.limit = rest.limit === '' ? undefined : Number(rest.limit)
}
Expand Down Expand Up @@ -1741,7 +1767,12 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
}

const projection = operation === 'servicenow_read_record' ? readFields : returnFields
return { ...rest, fields: projection || undefined }
const toolFields = toolCallArg(context, fields)
const toolProjection =
typeof toolFields === 'string' && !toolFields.trim().startsWith('{')
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
? toolFields
: undefined
return { ...rest, fields: projection || toolProjection || undefined }
},
},
},
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/hubspot/add_list_memberships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
HubSpotAddListMembershipsResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotAddListMemberships')

Expand Down Expand Up @@ -44,7 +45,8 @@ export const hubspotAddListMembershipsTool: ToolConfig<
},

request: {
url: (params) => `https://api.hubapi.com/crm/v3/lists/${params.listId.trim()}/memberships/add`,
url: (params) =>
`https://api.hubapi.com/crm/v3/lists/${safeUrlPathSegment(params.listId, 'listId')}/memberships/add`,
method: 'PUT',
headers: (params) => {
if (!params.accessToken) {
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/tools/hubspot/create_association.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
HubSpotCreateAssociationResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotCreateAssociation')

Expand Down Expand Up @@ -71,8 +72,8 @@ export const hubspotCreateAssociationTool: ToolConfig<

request: {
url: (params) => {
const from = `${encodeURIComponent(params.objectType.trim())}/${encodeURIComponent(params.objectId.trim())}`
const to = `${encodeURIComponent(params.toObjectType.trim())}/${encodeURIComponent(params.toObjectId.trim())}`
const from = `${safeUrlPathSegment(params.objectType, 'objectType')}/${safeUrlPathSegment(params.objectId, 'objectId')}`
const to = `${safeUrlPathSegment(params.toObjectType, 'toObjectType')}/${safeUrlPathSegment(params.toObjectId, 'toObjectId')}`

return params.associationTypeId != null
? `https://api.hubapi.com/crm/v4/objects/${from}/associations/${to}`
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/tools/hubspot/delete_association.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
HubSpotDeleteAssociationResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotDeleteAssociation')

Expand Down Expand Up @@ -56,8 +57,8 @@ export const hubspotDeleteAssociationTool: ToolConfig<

request: {
url: (params) => {
const from = `${encodeURIComponent(params.objectType.trim())}/${encodeURIComponent(params.objectId.trim())}`
const to = `${encodeURIComponent(params.toObjectType.trim())}/${encodeURIComponent(params.toObjectId.trim())}`
const from = `${safeUrlPathSegment(params.objectType, 'objectType')}/${safeUrlPathSegment(params.objectId, 'objectId')}`
const to = `${safeUrlPathSegment(params.toObjectType, 'toObjectType')}/${safeUrlPathSegment(params.toObjectId, 'toObjectId')}`
return `https://api.hubapi.com/crm/v4/objects/${from}/associations/${to}`
},
method: 'DELETE',
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/hubspot/delete_company.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
HubSpotDeleteCompanyResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotDeleteCompany')

Expand Down Expand Up @@ -37,7 +38,8 @@ export const hubspotDeleteCompanyTool: ToolConfig<
},

request: {
url: (params) => `https://api.hubapi.com/crm/v3/objects/companies/${params.companyId.trim()}`,
url: (params) =>
`https://api.hubapi.com/crm/v3/objects/companies/${safeUrlPathSegment(params.companyId, 'companyId')}`,
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/hubspot/delete_contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
HubSpotDeleteContactResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotDeleteContact')

Expand Down Expand Up @@ -37,7 +38,8 @@ export const hubspotDeleteContactTool: ToolConfig<
},

request: {
url: (params) => `https://api.hubapi.com/crm/v3/objects/contacts/${params.contactId.trim()}`,
url: (params) =>
`https://api.hubapi.com/crm/v3/objects/contacts/${safeUrlPathSegment(params.contactId, 'contactId')}`,
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/hubspot/delete_deal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import type { HubSpotDeleteDealParams, HubSpotDeleteDealResponse } from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotDeleteDeal')

Expand Down Expand Up @@ -32,7 +33,8 @@ export const hubspotDeleteDealTool: ToolConfig<HubSpotDeleteDealParams, HubSpotD
},

request: {
url: (params) => `https://api.hubapi.com/crm/v3/objects/deals/${params.dealId.trim()}`,
url: (params) =>
`https://api.hubapi.com/crm/v3/objects/deals/${safeUrlPathSegment(params.dealId, 'dealId')}`,
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/hubspot/delete_line_item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
HubSpotDeleteLineItemResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotDeleteLineItem')

Expand Down Expand Up @@ -37,7 +38,8 @@ export const hubspotDeleteLineItemTool: ToolConfig<
},

request: {
url: (params) => `https://api.hubapi.com/crm/v3/objects/line_items/${params.lineItemId.trim()}`,
url: (params) =>
`https://api.hubapi.com/crm/v3/objects/line_items/${safeUrlPathSegment(params.lineItemId, 'lineItemId')}`,
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/hubspot/delete_ticket.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import type { HubSpotDeleteTicketParams, HubSpotDeleteTicketResponse } from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotDeleteTicket')

Expand Down Expand Up @@ -34,7 +35,8 @@ export const hubspotDeleteTicketTool: ToolConfig<
},

request: {
url: (params) => `https://api.hubapi.com/crm/v3/objects/tickets/${params.ticketId.trim()}`,
url: (params) =>
`https://api.hubapi.com/crm/v3/objects/tickets/${safeUrlPathSegment(params.ticketId, 'ticketId')}`,
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/tools/hubspot/get_appointment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
} from '@/tools/hubspot/types'
import { APPOINTMENT_OBJECT_OUTPUT } from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotGetAppointment')

Expand Down Expand Up @@ -59,7 +60,7 @@ export const hubspotGetAppointmentTool: ToolConfig<

request: {
url: (params) => {
const baseUrl = `https://api.hubapi.com/crm/v3/objects/appointments/${params.appointmentId.trim()}`
const baseUrl = `https://api.hubapi.com/crm/v3/objects/appointments/${safeUrlPathSegment(params.appointmentId, 'appointmentId')}`
const queryParams = new URLSearchParams()
if (params.idProperty) queryParams.append('idProperty', params.idProperty)
if (params.properties) queryParams.append('properties', params.properties)
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/tools/hubspot/get_association_labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
HubSpotGetAssociationLabelsResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
import { safeUrlPathSegment } from '@/tools/url-path'

const logger = createLogger('HubSpotGetAssociationLabels')

Expand Down Expand Up @@ -45,8 +46,8 @@ export const hubspotGetAssociationLabelsTool: ToolConfig<

request: {
url: (params) => {
const from = encodeURIComponent(params.objectType.trim())
const to = encodeURIComponent(params.toObjectType.trim())
const from = safeUrlPathSegment(params.objectType, 'objectType')
const to = safeUrlPathSegment(params.toObjectType, 'toObjectType')
return `https://api.hubapi.com/crm/v4/associations/${from}/${to}/labels`
},
method: 'GET',
Expand Down
Loading
Loading