mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 00:03:00 +00:00
refactor: list the instance's resource types when the search finds nothing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8eec70a3ab
commit
03f4b632ec
@@ -20,25 +20,9 @@ import { editCodeToolWithDiff, searchResourceTypes } from './core'
|
||||
describe('searchResourceTypes', () => {
|
||||
const syncedNames = Array.from({ length: 30 }, (_, i) => `type_${i}`)
|
||||
|
||||
// The similarity index trails the table by up to a day, and builds without embeddings
|
||||
// have none, so a type the query names must still be found.
|
||||
it('finds a type the query names when the similarity search misses it', async () => {
|
||||
vi.spyOn(ResourceService, 'queryResourceTypes').mockResolvedValue([])
|
||||
vi.spyOn(ResourceService, 'listResourceTypeNames').mockResolvedValue([...syncedNames, 'stripe'])
|
||||
vi.spyOn(ResourceService, 'getResourceType').mockResolvedValue({
|
||||
name: 'stripe',
|
||||
schema: { type: 'object' }
|
||||
})
|
||||
|
||||
const { resourceTypes, note } = await searchResourceTypes('Stripe payments', 'ws', 5)
|
||||
|
||||
expect(resourceTypes.map((rt) => rt.name)).toEqual(['stripe'])
|
||||
expect(note).toBeUndefined()
|
||||
})
|
||||
|
||||
// An empty result from a failed search says nothing about whether a type exists: read as
|
||||
// "none", it sends the model to define a type the instance already has.
|
||||
it('hands back the type names when the similarity search fails', async () => {
|
||||
// An empty search says nothing about whether a type exists (stale index, or no embeddings):
|
||||
// read as "none", it sends the model to define a type the instance already has.
|
||||
it('lists the instance types when the similarity search finds nothing', async () => {
|
||||
vi.spyOn(ResourceService, 'queryResourceTypes').mockRejectedValue(new Error('500'))
|
||||
vi.spyOn(ResourceService, 'listResourceTypeNames').mockResolvedValue([
|
||||
...syncedNames,
|
||||
@@ -49,23 +33,21 @@ describe('searchResourceTypes', () => {
|
||||
|
||||
expect(resourceTypes).toEqual([])
|
||||
expect(note).toContain('postgresql')
|
||||
expect(note).not.toContain('type_0')
|
||||
expect(note).not.toContain('never synced')
|
||||
})
|
||||
|
||||
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: {} })
|
||||
it('returns the schema of a type the query names', async () => {
|
||||
vi.spyOn(ResourceService, 'queryResourceTypes').mockResolvedValue([])
|
||||
vi.spyOn(ResourceService, 'listResourceTypeNames').mockResolvedValue([...syncedNames, 'stripe'])
|
||||
vi.spyOn(ResourceService, 'getResourceType').mockResolvedValue({
|
||||
name: 'stripe',
|
||||
schema: { type: 'object' }
|
||||
})
|
||||
|
||||
const { resourceTypes } = await searchResourceTypes('stripe github', 'ws', 5)
|
||||
const { resourceTypes, note } = await searchResourceTypes('stripe', 'ws', 5)
|
||||
|
||||
expect(resourceTypes.map((rt) => rt.name)).toEqual(['stripe', 'github', 'a', 'b', 'c'])
|
||||
expect(resourceTypes.map((rt) => rt.name)).toEqual(['stripe'])
|
||||
expect(note).toBeUndefined()
|
||||
})
|
||||
|
||||
it('notes an instance that never synced with the hub', async () => {
|
||||
|
||||
@@ -73,64 +73,44 @@ export function formatResourceTypes(
|
||||
const UNSYNCED_RESOURCE_TYPE_COUNT = 20
|
||||
|
||||
/**
|
||||
* Similarity matches, led by any resource type the query names exactly: the similarity index
|
||||
* is rebuilt from the table once a day, and builds without embeddings have no index at all.
|
||||
* When the similarity search finds nothing, the instance's type names come back instead: the
|
||||
* index is rebuilt from the table once a day and builds without embeddings have none, so an
|
||||
* empty search does not mean no type fits. A query that is a type's name gets its schema.
|
||||
*/
|
||||
export async function searchResourceTypes(
|
||||
query: string,
|
||||
workspace: string,
|
||||
limit: number
|
||||
): Promise<{ resourceTypes: Pick<ResourceType, 'name' | 'schema'>[]; note?: string }> {
|
||||
let similarityFailed = false
|
||||
const [similar, names] = await Promise.all([
|
||||
ResourceService.queryResourceTypes({ workspace, text: query, limit }).catch(() => {
|
||||
similarityFailed = true
|
||||
return []
|
||||
}),
|
||||
ResourceService.listResourceTypeNames({ workspace })
|
||||
.then((listed) => new Set(listed))
|
||||
.catch(() => undefined)
|
||||
])
|
||||
|
||||
const normalized = query.trim().toLowerCase()
|
||||
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(
|
||||
[...candidates]
|
||||
.filter((name) => names?.has(name))
|
||||
.slice(0, limit)
|
||||
.map(
|
||||
(name) =>
|
||||
similar.find((rt) => rt.name === name) ??
|
||||
ResourceService.getResourceType({ workspace, path: name }).catch(() => undefined)
|
||||
)
|
||||
const similar = await ResourceService.queryResourceTypes({ workspace, text: query, limit }).catch(
|
||||
() => []
|
||||
)
|
||||
const named = exact.flatMap((rt) => (rt ? [{ name: rt.name, schema: rt.schema }] : []))
|
||||
const resourceTypes = [
|
||||
...named,
|
||||
...similar.filter((rt) => !named.some((n) => n.name === rt.name))
|
||||
].slice(0, limit)
|
||||
if (similar.length > 0) {
|
||||
return { resourceTypes: similar }
|
||||
}
|
||||
|
||||
// Without the similarity search, an empty result says nothing about whether a type exists,
|
||||
// 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. ${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}`
|
||||
: undefined
|
||||
].filter((note) => note !== undefined)
|
||||
const names = [
|
||||
...new Set(await ResourceService.listResourceTypeNames({ workspace }).catch(() => []))
|
||||
]
|
||||
const name = query.trim().toLowerCase()
|
||||
const named = names.includes(name)
|
||||
? await ResourceService.getResourceType({ workspace, path: name }).catch(() => undefined)
|
||||
: undefined
|
||||
if (named) {
|
||||
return { resourceTypes: [{ name: named.name, schema: named.schema }] }
|
||||
}
|
||||
if (names.length === 0) {
|
||||
return { resourceTypes: [] }
|
||||
}
|
||||
|
||||
return { resourceTypes, note: notes.length > 0 ? notes.join('\n\n') : undefined }
|
||||
const unsynced =
|
||||
names.length < UNSYNCED_RESOURCE_TYPE_COUNT
|
||||
? `\n\nOnly ${names.length} 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}`
|
||||
: ''
|
||||
return {
|
||||
resourceTypes: [],
|
||||
note: `The resource types on this instance are: ${names.join(', ')}. Search again with one of these names to get its schema.${unsynced}`
|
||||
}
|
||||
}
|
||||
|
||||
export const SUPPORTED_CHAT_SCRIPT_LANGUAGES = [
|
||||
|
||||
Reference in New Issue
Block a user