FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(sixtyfour,enrow): stop nulling model-supplied struct, retry transient poll failures by waleedlatif1 · Pull Request #7261 · simstudioai/sim · GitHub

176 changes: 176 additions & 0 deletions apps/sim/blocks/blocks/sixtyfour.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { SixtyfourBlock } from '@/blocks/blocks/sixtyfour'
import { transformBlockTool } from '@/providers/utils'
import { sixtyfourEnrichCompanyTool } from '@/tools/sixtyfour/enrich_company'
import { sixtyfourEnrichLeadTool } from '@/tools/sixtyfour/enrich_lead'
import { sixtyfourFindEmailTool } from '@/tools/sixtyfour/find_email'
import { sixtyfourFindPhoneTool } from '@/tools/sixtyfour/find_phone'
import type { ExecutableToolConfig } from '@/tools/types'

const SIXTYFOUR_TOOLS: Record<string, ExecutableToolConfig> = {
sixtyfour_find_phone: sixtyfourFindPhoneTool as ExecutableToolConfig,
sixtyfour_find_email: sixtyfourFindEmailTool as ExecutableToolConfig,
sixtyfour_enrich_lead: sixtyfourEnrichLeadTool as ExecutableToolConfig,
sixtyfour_enrich_company: sixtyfourEnrichCompanyTool as ExecutableToolConfig,
}

const buildParams = SixtyfourBlock.tools.config!.params!

/** The shape the generic (canvas) handler forwards: raw inputs overlaid by the mapper. */
const resolve = (inputs: Record<string, unknown>) => ({ ...inputs, ...buildParams(inputs) })

/**
* Builds the agent-path `paramsTransform` the way `transformBlockTool` does, so
* assertions run against the exact function that rewrites a model's tool-call
* arguments before `executeTool` sees them.
*/
async function agentParamsTransform(
operation: string,
blockParams: Record<string, unknown>
): Promise<(args: Record<string, unknown>) => Record<string, unknown>> {
const tool = await transformBlockTool(
{ type: 'sixtyfour', operation, params: { operation, ...blockParams } },
{
selectedOperation: operation,
getAllBlocks: () => [SixtyfourBlock],
getTool: (id: string) => SIXTYFOUR_TOOLS[id],
}
)
if (!tool?.paramsTransform) throw new Error(`No paramsTransform for ${operation}`)
return tool.paramsTransform as (args: Record<string, unknown>) => Record<string, unknown>
}

describe('SixtyfourBlock agent path', () => {
it('keeps the model-supplied struct for enrich_lead', async () => {
const transform = await agentParamsTransform('enrich_lead', { apiKey: 'key' })

const sent = transform({
operation: 'enrich_lead',
apiKey: 'key',
leadInfo: '{"name":"John Doe"}',
struct: '{"email":"Work email address"}',
})

expect(sent.struct).toBe('{"email":"Work email address"}')
expect(sent.leadInfo).toBe('{"name":"John Doe"}')
})

it('keeps the model-supplied struct for enrich_company', async () => {
const transform = await agentParamsTransform('enrich_company', { apiKey: 'key' })

const sent = transform({
operation: 'enrich_company',
apiKey: 'key',
targetCompany: '{"name":"Acme Inc"}',
struct: '{"website":"Company website URL"}',
})

expect(sent.struct).toBe('{"website":"Company website URL"}')
expect(sent.targetCompany).toBe('{"name":"Acme Inc"}')
})

it.each([
['null', null],
['an empty string', ''],
])('keeps the model-supplied struct when the subBlock resolves to %s', async (_label, empty) => {
const transform = await agentParamsTransform('enrich_lead', { apiKey: 'key' })

const sent = transform({
operation: 'enrich_lead',
apiKey: 'key',
leadInfo: '{"name":"John Doe"}',
leadStruct: empty,
struct: '{"email":"Work email address"}',
})

expect(sent.struct).toBe('{"email":"Work email address"}')
})

it('keeps the model-supplied company struct when the subBlock resolves to null', async () => {
const transform = await agentParamsTransform('enrich_company', { apiKey: 'key' })

const sent = transform({
operation: 'enrich_company',
apiKey: 'key',
companyStruct: null,
targetCompany: '{"name":"Acme Inc"}',
struct: '{"website":"Company website URL"}',
})

expect(sent.struct).toBe('{"website":"Company website URL"}')
expect(sent.targetCompany).toBe('{"name":"Acme Inc"}')
})

it('forwards an explicit false switch without coercing an untouched one', () => {
const mapped = buildParams({
operation: 'enrich_company',
apiKey: 'key',
targetCompany: '{"name":"Acme Inc"}',
companyStruct: '{"website":"Company website URL"}',
findPeople: false,
fullOrgChart: null,
})

// A switch the user actually turned off must still reach the tool as `false`.
expect(mapped.findPeople).toBe(false)
// An untouched one must not be written at all, so `Boolean(null)` cannot
// force `false` over a `true` the model sent.
expect('fullOrgChart' in mapped).toBe(false)
})

it('lets a configured block value win over the model for enrich_lead', async () => {
const transform = await agentParamsTransform('enrich_lead', {
apiKey: 'key',
leadStruct: '{"phone":"Phone number"}',
})

const sent = transform({
operation: 'enrich_lead',
apiKey: 'key',
leadStruct: '{"phone":"Phone number"}',
leadInfo: '{"name":"John Doe"}',
})

expect(sent.struct).toBe('{"phone":"Phone number"}')
})
})

describe('SixtyfourBlock canvas path', () => {
it('maps the lead subBlocks onto the tool params', () => {
const params = resolve({
operation: 'enrich_lead',
apiKey: 'key',
leadInfo: '{"name":"John Doe"}',
leadStruct: '{"email":"Work email address"}',
leadResearchPlan: 'Check LinkedIn first',
})

expect(params.leadInfo).toBe('{"name":"John Doe"}')
expect(params.struct).toBe('{"email":"Work email address"}')
expect(params.researchPlan).toBe('Check LinkedIn first')
})

it('maps the company subBlocks onto the tool params', () => {
const params = resolve({
operation: 'enrich_company',
apiKey: 'key',
targetCompany: '{"name":"Acme Inc"}',
companyStruct: '{"website":"Company website URL"}',
companyLeadStruct: '{"name":"Full name"}',
companyResearchPlan: 'Start from the careers page',
})

expect(params.targetCompany).toBe('{"name":"Acme Inc"}')
expect(params.struct).toBe('{"website":"Company website URL"}')
expect(params.leadStruct).toBe('{"name":"Full name"}')
expect(params.researchPlan).toBe('Start from the careers page')
})

it('renames the lookup inputs for the find operations', () => {
expect(resolve({ operation: 'find_phone', emailInput: 'a@b.com' }).email).toBe('a@b.com')
expect(resolve({ operation: 'find_email', phoneInput: '+15551234' }).phone).toBe('+15551234')
})
})
35 changes: 29 additions & 6 deletions apps/sim/blocks/blocks/sixtyfour.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -220,22 +220,45 @@ export const SixtyfourBlock: BlockConfig = {
],
config: {
tool: (params) => `sixtyfour_${params.operation}`,
/**
* Renames the operation-scoped subBlock ids onto the tool's param names.
*
* Every assignment is guarded on the source being present — see `present` below
* for why `undefined` alone is not a sufficient test. The agent path
* overlays this return on top of the model's own tool-call arguments, and
* the model supplies the *tool* names (`struct`, `targetCompany`), not the
* subBlock names (`leadStruct`, `companyStruct`) — so an unguarded write
* would set the required `struct` to `undefined` and drop what the model
* sent. A configured block value still wins, because it is present.
*/
params: (params) => {
const result: Record<string, unknown> = {}

/**
* Whether a source value is present enough to overwrite what the model sent.
*
* `undefined` alone is not sufficient: the serializer stores every untouched
* subBlock as `params[id] ?? null`, so an unfilled field arrives as `null`,
* and a cleared one as `''`. Both would otherwise be written over a valid
* model argument — `null` reaching the tool as an invalid required param,
* `''` failing its `JSON.parse` with "struct must be valid JSON". This is
* the same guard `trigger_dev`'s `scoped()` and the `enrow` mapper use.
*/
const present = (value: unknown) => value !== undefined && value !== null && value !== ''

if (params.operation === 'find_phone') {
if (params.emailInput) result.email = params.emailInput
} else if (params.operation === 'find_email') {
if (params.phoneInput) result.phone = params.phoneInput
} else if (params.operation === 'enrich_lead') {
result.leadInfo = params.leadInfo
result.struct = params.leadStruct
if (present(params.leadInfo)) result.leadInfo = params.leadInfo
if (present(params.leadStruct)) result.struct = params.leadStruct
if (params.leadResearchPlan) result.researchPlan = params.leadResearchPlan
} else if (params.operation === 'enrich_company') {
result.targetCompany = params.targetCompany
result.struct = params.companyStruct
if (params.findPeople !== undefined) result.findPeople = Boolean(params.findPeople)
if (params.fullOrgChart !== undefined) result.fullOrgChart = Boolean(params.fullOrgChart)
if (present(params.targetCompany)) result.targetCompany = params.targetCompany
if (present(params.companyStruct)) result.struct = params.companyStruct
if (present(params.findPeople)) result.findPeople = Boolean(params.findPeople)
if (present(params.fullOrgChart)) result.fullOrgChart = Boolean(params.fullOrgChart)
if (params.peopleFocusPrompt) result.peopleFocusPrompt = params.peopleFocusPrompt
if (params.companyLeadStruct) result.leadStruct = params.companyLeadStruct
if (params.companyResearchPlan) result.researchPlan = params.companyResearchPlan
Expand Down
Loading
Loading

Back | FazBrowse Home | New Git URL