mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 16:01:42 +00:00
fix: match integration suggestions on slug words instead of substrings
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1453,6 +1453,75 @@ describe('createSearchHubScriptsTool', () => {
|
||||
|
||||
expect(JSON.parse(raw)).toEqual({ results: [], suggested_integrations: ['stripe'] })
|
||||
})
|
||||
|
||||
// Matching a query word anywhere inside a slug turns every short English word into
|
||||
// a hit — `for` in salesforce, `the` in basis_theory — so a task that names no
|
||||
// integration came back with a confident-looking list of them.
|
||||
it('suggests nothing for a request that names no integration', async () => {
|
||||
const { ScriptService, IntegrationService } = await import('$lib/gen')
|
||||
Object.assign(ScriptService, { queryHubScripts: vi.fn(async () => []) })
|
||||
Object.assign(IntegrationService, {
|
||||
listHubIntegrations: vi.fn(async () => [
|
||||
{ name: 'salesforce' },
|
||||
{ name: 'basis_theory' },
|
||||
{ name: 'hackernews' },
|
||||
{ name: 's3' }
|
||||
])
|
||||
})
|
||||
|
||||
const { createSearchHubScriptsTool, clearHubIntegrationsCache } = await import('./shared')
|
||||
clearHubIntegrationsCache()
|
||||
const raw = await createSearchHubScriptsTool().fn({
|
||||
args: { query: 'list all the invoices for the new month' },
|
||||
toolId: 't1',
|
||||
toolCallbacks: { setToolStatus: vi.fn() }
|
||||
} as any)
|
||||
|
||||
expect(JSON.parse(raw).suggested_integrations).toEqual([])
|
||||
})
|
||||
|
||||
// Google's integrations are all a compressed `g` plus the product word, so the
|
||||
// word a user actually says starts one character into the slug.
|
||||
it('reaches an integration whose slug compresses the vendor name', async () => {
|
||||
const { ScriptService, IntegrationService } = await import('$lib/gen')
|
||||
Object.assign(ScriptService, { queryHubScripts: vi.fn(async () => []) })
|
||||
Object.assign(IntegrationService, {
|
||||
listHubIntegrations: vi.fn(async () => [
|
||||
{ name: 'gsheets' },
|
||||
{ name: 'gdrive' },
|
||||
{ name: 'smartsheet' }
|
||||
])
|
||||
})
|
||||
|
||||
const { createSearchHubScriptsTool, clearHubIntegrationsCache } = await import('./shared')
|
||||
clearHubIntegrationsCache()
|
||||
const raw = await createSearchHubScriptsTool().fn({
|
||||
args: { query: 'add a row to a google sheet' },
|
||||
toolId: 't1',
|
||||
toolCallbacks: { setToolStatus: vi.fn() }
|
||||
} as any)
|
||||
|
||||
expect(JSON.parse(raw).suggested_integrations).toEqual(['gsheets'])
|
||||
})
|
||||
|
||||
// A slug shorter than the token floor is only reachable by an exact match.
|
||||
it('still reaches a two-character integration slug', async () => {
|
||||
const { ScriptService, IntegrationService } = await import('$lib/gen')
|
||||
Object.assign(ScriptService, { queryHubScripts: vi.fn(async () => []) })
|
||||
Object.assign(IntegrationService, {
|
||||
listHubIntegrations: vi.fn(async () => [{ name: 's3' }, { name: 'salesforce' }])
|
||||
})
|
||||
|
||||
const { createSearchHubScriptsTool, clearHubIntegrationsCache } = await import('./shared')
|
||||
clearHubIntegrationsCache()
|
||||
const raw = await createSearchHubScriptsTool().fn({
|
||||
args: { query: 'upload a file to s3' },
|
||||
toolId: 't1',
|
||||
toolCallbacks: { setToolStatus: vi.fn() }
|
||||
} as any)
|
||||
|
||||
expect(JSON.parse(raw).suggested_integrations).toEqual(['s3'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getHubIntegrationTool', () => {
|
||||
|
||||
@@ -1271,15 +1271,37 @@ async function suggestHubIntegrations(query: string): Promise<string[]> {
|
||||
const tokens = query
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9]+/)
|
||||
.filter((t) => t.length > 2)
|
||||
.filter((t) => t.length >= 2)
|
||||
return hubIntegrationsCache
|
||||
.filter((name) => {
|
||||
const slug = name.toLowerCase()
|
||||
return tokens.some((t) => slug.includes(t) || (slug.length > 2 && t.includes(slug)))
|
||||
})
|
||||
.filter((name) => tokens.some((t) => tokenMatchesSlug(t, name.toLowerCase())))
|
||||
.slice(0, MAX_SUGGESTED_INTEGRATIONS)
|
||||
}
|
||||
|
||||
/** Matches a query word against a slug on word boundaries rather than by bare
|
||||
* substring. A substring test reads every three-letter English word as a hit —
|
||||
* `for` in sales*for*ce, `the` in basis_*the*ory — so a request that names no
|
||||
* integration still came back with five confident-looking ones. Short tokens must
|
||||
* equal a slug or one of its parts, which is also what reaches the two-character
|
||||
* slugs (`s3`, `wiz`) that a length filter alone hides. */
|
||||
function tokenMatchesSlug(token: string, slug: string): boolean {
|
||||
const parts = slug.split(/[_-]/).filter(Boolean)
|
||||
if (slug === token || parts.includes(token)) {
|
||||
return true
|
||||
}
|
||||
if (token.length < 4) {
|
||||
return false
|
||||
}
|
||||
const extends_ = (a: string, b: string) => a.startsWith(b) || b.startsWith(a)
|
||||
return (
|
||||
parts.some((p) => p.length >= 4 && extends_(p, token)) ||
|
||||
(slug.length >= 4 && extends_(slug, token)) ||
|
||||
// A whole family of slugs compresses the vendor to one letter in front of the
|
||||
// product word, so the word the user actually says starts one character in:
|
||||
// "google sheet" has to reach `gsheets`, "google drive" `gdrive`.
|
||||
parts.some((p) => p.length >= 5 && extends_(p.slice(1), token))
|
||||
)
|
||||
}
|
||||
|
||||
export const clearHubIntegrationsCache = () => {
|
||||
hubIntegrationsCache = undefined
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user