diff --git a/frontend/src/lib/components/copilot/chat/script/core.test.ts b/frontend/src/lib/components/copilot/chat/script/core.test.ts index 627fe8c0dd..6e665b064d 100644 --- a/frontend/src/lib/components/copilot/chat/script/core.test.ts +++ b/frontend/src/lib/components/copilot/chat/script/core.test.ts @@ -49,6 +49,23 @@ describe('searchResourceTypes', () => { expect(resourceTypes).toEqual([]) expect(note).toContain('postgresql') + expect(note).not.toContain('type_0') + }) + + it('keeps every type the query names ahead of the other matches', async () => { + vi.spyOn(ResourceService, 'queryResourceTypes').mockResolvedValue( + ['a', 'b', 'c', 'd', 'stripe'].map((name) => ({ name, score: 0.8 })) + ) + vi.spyOn(ResourceService, 'listResourceTypeNames').mockResolvedValue([ + ...syncedNames, + 'stripe', + 'github' + ]) + vi.spyOn(ResourceService, 'getResourceType').mockResolvedValue({ name: 'github', schema: {} }) + + const { resourceTypes } = await searchResourceTypes('stripe github', 'ws', 5) + + expect(resourceTypes.map((rt) => rt.name)).toEqual(['stripe', 'github', 'a', 'b', 'c']) }) it('notes an instance that never synced with the hub', async () => { diff --git a/frontend/src/lib/components/copilot/chat/script/core.ts b/frontend/src/lib/components/copilot/chat/script/core.ts index 5e7d24c616..0bdb02b0f2 100644 --- a/frontend/src/lib/components/copilot/chat/script/core.ts +++ b/frontend/src/lib/components/copilot/chat/script/core.ts @@ -93,30 +93,37 @@ export async function searchResourceTypes( ]) const normalized = query.trim().toLowerCase() - const candidates = new Set([ - normalized.replace(/[\s-]+/g, '_'), - ...normalized.split(/[^a-z0-9_]+/) - ]) - const exactNames = [...candidates].filter( - (name) => names?.has(name) && !similar.some((rt) => rt.name === name) - ) + const words = normalized.split(/[^a-z0-9_]+/).filter((word) => word.length > 0) + const candidates = new Set([normalized.replace(/[\s-]+/g, '_'), ...words]) const exact = await Promise.all( - exactNames + [...candidates] + .filter((name) => names?.has(name)) .slice(0, limit) - .map((name) => - ResourceService.getResourceType({ workspace, path: name }).catch(() => undefined) + .map( + (name) => + similar.find((rt) => rt.name === name) ?? + ResourceService.getResourceType({ workspace, path: name }).catch(() => undefined) ) ) + const named = exact.flatMap((rt) => (rt ? [{ name: rt.name, schema: rt.schema }] : [])) const resourceTypes = [ - ...exact.flatMap((rt) => (rt ? [{ name: rt.name, schema: rt.schema }] : [])), - ...similar + ...named, + ...similar.filter((rt) => !named.some((n) => n.name === rt.name)) ].slice(0, limit) // Without the similarity search, an empty result says nothing about whether a type exists, - // so the model gets the names to search again with rather than concluding there is none. + // so the model gets the names close to its query rather than concluding there is none. + const related = + similarityFailed && names + ? [...names] + .filter((name) => + words.some((word) => word.length >= 3 && (name.includes(word) || word.includes(name))) + ) + .slice(0, 30) + : [] const notes = [ similarityFailed && names - ? `The similarity search is unavailable on this instance, so only a type named in the query can be found. Search again with one of the instance's resource types: ${[...names].join(', ')}.` + ? `The similarity search is unavailable on this instance, so only a type named in the query can be found. ${related.length > 0 ? `Resource types whose names share a word with the query: ${related.join(', ')}.` : "Search again with the service's name."}` : undefined, names && names.size < UNSYNCED_RESOURCE_TYPE_COUNT ? `Only ${names.size} resource types exist on this instance, so it has most likely never synced with the Windmill Hub, which has types for most services. ${HUB_SYNC_INSTRUCTIONS}`