@drewswiredin/backstage-plugin-assistants-backend-module-semantic-search 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/action.cjs.js +13 -5
- package/dist/action.cjs.js.map +1 -1
- package/dist/rank.cjs.js +17 -0
- package/dist/rank.cjs.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -203,7 +203,7 @@ fields its collator attached. The table is a rebuildable copy, not migrated
|
|
|
203
203
|
data.
|
|
204
204
|
|
|
205
205
|
**Retrieval.** `search-knowledge` takes the question, up to three alternative
|
|
206
|
-
phrasings, and optionally the `types` to search (otherwise every type that is
|
|
206
|
+
phrasings (it uses the first three distinct ones and ignores the rest), and optionally the `types` to search (otherwise every type that is
|
|
207
207
|
on by default, plus unnamed sources). It builds one semantic list per
|
|
208
208
|
phrasing, a keyword list, a rare-word list, and a fuzzy list, each keeping
|
|
209
209
|
its best 30 chunks of every type, so a large source cannot fill a list on its
|
package/dist/action.cjs.js
CHANGED
|
@@ -37,11 +37,18 @@ Each result has a title (its heading path), an excerpt, a location, its source t
|
|
|
37
37
|
schema: {
|
|
38
38
|
input: (z) => z.object({
|
|
39
39
|
query: z.string().min(1).describe("The question or topic to find"),
|
|
40
|
-
alternatives: z.array(z.string()).
|
|
40
|
+
alternatives: z.array(z.string()).optional().describe(
|
|
41
|
+
"Up to three other phrasings or terms for the same thing; extra ones are ignored"
|
|
42
|
+
),
|
|
41
43
|
types: z.array(z.string()).optional().describe(
|
|
42
44
|
"Search only these sources (search document types); omit to search the default ones"
|
|
43
45
|
),
|
|
44
|
-
limit: z.number().int().min(1).
|
|
46
|
+
limit: z.number().int().min(1).optional().default(maxResults).describe(
|
|
47
|
+
`Number of results to return, at most ${Math.max(
|
|
48
|
+
MAX_LIMIT,
|
|
49
|
+
maxResults
|
|
50
|
+
)}`
|
|
51
|
+
)
|
|
45
52
|
}),
|
|
46
53
|
output: (z) => z.object({
|
|
47
54
|
results: z.array(
|
|
@@ -66,7 +73,8 @@ Each result has a title (its heading path), an excerpt, a location, its source t
|
|
|
66
73
|
);
|
|
67
74
|
}
|
|
68
75
|
const filter = ranking.selectTypes(types, await store.types(), input.types);
|
|
69
|
-
const
|
|
76
|
+
const limit = Math.min(input.limit, Math.max(MAX_LIMIT, maxResults));
|
|
77
|
+
const phrasings = rank.phrasingsOf(input.query, input.alternatives);
|
|
70
78
|
const words = rank.queryWords(phrasings);
|
|
71
79
|
const joined = rank.joinedWords(phrasings).filter((w) => !words.includes(w));
|
|
72
80
|
const [{ embeddings }, frequencies] = await Promise.all([
|
|
@@ -131,9 +139,9 @@ Each result has a title (its heading path), an excerpt, a location, its source t
|
|
|
131
139
|
phrasings,
|
|
132
140
|
types: filter.types,
|
|
133
141
|
candidates,
|
|
134
|
-
maxResults:
|
|
142
|
+
maxResults: limit
|
|
135
143
|
});
|
|
136
|
-
const results = ranked.filter((candidate) => ids.delete(candidate.id)).slice(0,
|
|
144
|
+
const results = ranked.filter((candidate) => ids.delete(candidate.id)).slice(0, limit).map((candidate) => ({
|
|
137
145
|
title: candidate.title,
|
|
138
146
|
location: candidate.location,
|
|
139
147
|
...candidate.type ? { type: candidate.type } : {},
|
package/dist/action.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.cjs.js","sources":["../src/action.ts"],"sourcesContent":["import type {\n AuthService,\n BackstageCredentials,\n} from '@backstage/backend-plugin-api';\nimport type { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport type { CatalogApi } from '@backstage/catalog-client';\nimport { embedMany, type EmbeddingModel } from 'ai';\nimport type { KnowledgeTypeSettings } from './config';\nimport { fuseRankings, joinedWords, queryWords, type RankedList } from './rank';\nimport {\n selectTypes,\n typeSettingsOf,\n type KnowledgeCandidate,\n type KnowledgeRanker,\n} from './ranking';\nimport type { KnowledgeStore } from './store';\n\n/** Candidates per type in each retrieval list, before fusion and permission filtering. */\nconst CANDIDATES = 30;\n/** Fused candidates checked for permission and ranked; over-fetched so filtering and type caps rarely empty the page. */\nconst FUSED = 60;\n/** The most results a caller may ask for, unless `maxResults` is configured higher. */\nconst MAX_LIMIT = 20;\nconst EXCERPT_CHARS = 1500;\n/** A word in this many chunks or fewer is rare: strong evidence of what the user means. */\nconst RARE_MAX_CHUNKS = 3;\n\nexport interface SearchKnowledgeDeps {\n actionsRegistry: ActionsRegistryService;\n store: KnowledgeStore;\n catalog: CatalogApi;\n auth: AuthService;\n embeddingModel: EmbeddingModel;\n types: Map<string, KnowledgeTypeSettings>;\n maxResults: number;\n /** The ranker in effect: the default, or one a module set. */\n ranker: () => KnowledgeRanker;\n}\n\n/**\n * Registers `search-knowledge`: hybrid retrieval over the knowledge index. Each\n * phrasing is embedded and matched by meaning; the query words are matched\n * exactly (full-text, any word) and, for words absent from the index, by\n * near spelling (trigram). Words found in only a few chunks get a list of\n * their own, so a rare name or acronym buried in a paragraph about something\n * else is not outvoted by sections that are merely near the question.\n *\n * Each list keeps its best chunks of every document type, so a large source\n * cannot fill a list on its own. The lists are fused by reciprocal rank with\n * one vote per method, each chunk's score multiplied by its type's weight:\n * the phrasings share the meaning vote, so extra alternatives broaden recall\n * without drowning out exact matches. Results gated by a catalog entity are\n * filtered to entities the caller can read; the rest are visible to every\n * assistant user, as they are in Backstage search. Repeated text is shown\n * once, and the ranker picks the results from what remains.\n */\nexport function registerSearchKnowledgeAction({\n actionsRegistry,\n store,\n catalog,\n auth,\n embeddingModel,\n types,\n maxResults,\n ranker,\n}: SearchKnowledgeDeps): void {\n actionsRegistry.register({\n name: 'search-knowledge',\n title: 'Search Knowledge',\n attributes: { destructive: false, readOnly: true, idempotent: true },\n description: `Search everything the organization's Backstage search indexes (TechDocs, the software catalog, and any other source it collects, such as Confluence or wiki pages) by meaning, not only by exact words. Best for questions in the user's own words: it finds a page even when the question is worded differently from it (\"fix errored messages\" finds \"Retry dead-letter messages\") and tolerates misspelled names (\"Pilon\" finds \"Pylon\"). It returns the most relevant sections, not every match, and cannot filter by entity kind.\n${sourcesDescription(types)}\n\nHow to search the organization's docs and catalog:\n1. Start here, with the user's question.\n2. If the results don't clearly answer, also run a keyword search: the query tool (catalog and TechDocs at once) when it is available, otherwise search-techdocs and search-catalog. Keyword search finds rare names, acronyms and jargon that ranking by meaning can miss. For a bare name, term or acronym, run both searches from the start.\n3. Report something as missing only after both searches come back empty.\n\nPass the user's question as the \"query\" parameter. Pass up to three \"alternatives\": other phrasings, synonyms, or the internal terms the docs might use.\n\nEach result has a title (its heading path), an excerpt, a location, its source type, the fields the source attached (such as status, space, or last update: use them to judge how current it is), and, for catalog and TechDocs results, the entity ref. A location starting with /docs/ is a TechDocs page: pass it to read-techdocs to read the full page. Any other location is a link to the page or entity; cite it.`,\n schema: {\n input: z =>\n z.object({\n query: z.string().min(1).describe('The question or topic to find'),\n alternatives: z\n .array(z.string())\n .max(3)\n .optional()\n .describe('Other phrasings or terms for the same thing'),\n types: z\n .array(z.string())\n .optional()\n .describe(\n 'Search only these sources (search document types); omit to search the default ones',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .max(Math.max(MAX_LIMIT, maxResults))\n .optional()\n .default(maxResults)\n .describe('Number of results to return'),\n }),\n output: z =>\n z.object({\n results: z.array(\n z.object({\n title: z.string(),\n location: z.string(),\n type: z.string().optional(),\n entityRef: z.string().optional(),\n metadata: z\n .record(\n z.string(),\n z.union([z.string(), z.number(), z.boolean()]),\n )\n .optional(),\n excerpt: z.string(),\n }),\n ),\n }),\n },\n action: async ({ input, credentials }) => {\n if (!(await store.isReady())) {\n throw new Error(\n 'The knowledge index is still being built; meanwhile use keyword search: the query tool when it is available, otherwise search-techdocs and search-catalog.',\n );\n }\n\n const filter = selectTypes(types, await store.types(), input.types);\n const phrasings = [input.query, ...(input.alternatives ?? [])];\n const words = queryWords(phrasings);\n const joined = joinedWords(phrasings).filter(w => !words.includes(w));\n const [{ embeddings }, frequencies] = await Promise.all([\n embedMany({ model: embeddingModel, values: phrasings }),\n store.documentFrequencies(\n [...words, ...joined],\n RARE_MAX_CHUNKS + 1,\n filter,\n ),\n ]);\n const known = [...words, ...joined].filter(\n w => (frequencies.get(w) ?? 0) > 0,\n );\n const rare = known.filter(w => frequencies.get(w)! <= RARE_MAX_CHUNKS);\n const unknown = words.filter(w => frequencies.get(w) === 0);\n\n const lists: RankedList[] = await Promise.all([\n ...embeddings.map(async embedding => ({\n name: 'semantic' as const,\n hits: await store.nearest(embedding, CANDIDATES, filter),\n weight: 1 / embeddings.length,\n })),\n store\n .keyword(known, CANDIDATES, filter)\n .then(hits => ({ name: 'keyword' as const, hits, weight: 1 })),\n store\n .keyword(rare, CANDIDATES, filter)\n .then(hits => ({ name: 'rareWord' as const, hits, weight: 1 })),\n store\n .fuzzy(unknown, CANDIDATES, filter)\n .then(hits => ({ name: 'fuzzy' as const, hits, weight: 1 })),\n ]);\n\n const settingsOf = typeSettingsOf(types);\n const fused = fuseRankings(lists, type => settingsOf(type).weight).slice(\n 0,\n FUSED,\n );\n const byId = new Map(\n (await store.chunks(fused.map(hit => hit.id))).map(chunk => [\n chunk.id,\n chunk,\n ]),\n );\n const found = fused.flatMap(hit => {\n const chunk = byId.get(hit.id);\n return chunk ? [{ hit, chunk }] : [];\n });\n\n const readable = await readableEntityRefs(catalog, auth, credentials, [\n ...new Set(\n found.flatMap(({ chunk }) =>\n chunk.entityRef ? [chunk.entityRef] : [],\n ),\n ),\n ]);\n\n const seen = new Set<string>();\n const candidates: KnowledgeCandidate[] = [];\n for (const { hit, chunk } of found) {\n const key = `${chunk.location}|${chunk.title}`;\n if (\n (chunk.entityRef && !readable.has(chunk.entityRef)) ||\n seen.has(key) ||\n seen.has(chunk.content)\n ) {\n continue;\n }\n seen.add(key);\n seen.add(chunk.content);\n candidates.push({ ...chunk, score: hit.score, ranks: hit.ranks });\n }\n\n const ids = new Set(candidates.map(candidate => candidate.id));\n const ranked = await ranker()({\n query: input.query,\n phrasings,\n types: filter.types,\n candidates,\n maxResults: input.limit,\n });\n const results = ranked\n .filter(candidate => ids.delete(candidate.id))\n .slice(0, input.limit)\n .map(candidate => ({\n title: candidate.title,\n location: candidate.location,\n ...(candidate.type ? { type: candidate.type } : {}),\n ...(candidate.entityRef ? { entityRef: candidate.entityRef } : {}),\n ...(candidate.metadata ? { metadata: candidate.metadata } : {}),\n excerpt: candidate.content.slice(0, EXCERPT_CHARS),\n }));\n return { output: { results } };\n },\n });\n}\n\n/**\n * The tool description's paragraph on sources: the configured types, which\n * ones are searched only when named, and what each holds.\n */\nexport function sourcesDescription(\n types: Map<string, KnowledgeTypeSettings>,\n): string {\n const listed = [...types].filter(\n ([, settings]) => settings.enabled && settings.weight > 0,\n );\n const lines = listed.map(\n ([type, settings]) =>\n `- ${type}${settings.default ? '' : ' (only when named in \"types\")'}${\n settings.description ? `: ${settings.description}` : ''\n }`,\n );\n return [\n '',\n `Sources: pass \"types\" to search only the named search document types; without it the search covers every source except those marked as searched only when named. An unknown name returns the list of types.`,\n ...lines,\n ].join('\\n');\n}\n\n/**\n * The subset of entity refs the caller can read, asked of the catalog as the\n * caller — so the catalog's own permission policy decides, and a deleted\n * entity's leftover chunks never surface.\n */\nasync function readableEntityRefs(\n catalog: CatalogApi,\n auth: AuthService,\n credentials: BackstageCredentials,\n entityRefs: string[],\n): Promise<Set<string>> {\n if (entityRefs.length === 0) {\n return new Set();\n }\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: credentials,\n targetPluginId: 'catalog',\n });\n const { items } = await catalog.getEntitiesByRefs(\n { entityRefs, fields: ['kind', 'metadata.namespace', 'metadata.name'] },\n { token },\n );\n return new Set(entityRefs.filter((_, i) => items[i]));\n}\n"],"names":["selectTypes","queryWords","joinedWords","embedMany","typeSettingsOf","fuseRankings"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"action.cjs.js","sources":["../src/action.ts"],"sourcesContent":["import type {\n AuthService,\n BackstageCredentials,\n} from '@backstage/backend-plugin-api';\nimport type { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport type { CatalogApi } from '@backstage/catalog-client';\nimport { embedMany, type EmbeddingModel } from 'ai';\nimport type { KnowledgeTypeSettings } from './config';\nimport {\n fuseRankings,\n joinedWords,\n phrasingsOf,\n queryWords,\n type RankedList,\n} from './rank';\nimport {\n selectTypes,\n typeSettingsOf,\n type KnowledgeCandidate,\n type KnowledgeRanker,\n} from './ranking';\nimport type { KnowledgeStore } from './store';\n\n/** Candidates per type in each retrieval list, before fusion and permission filtering. */\nconst CANDIDATES = 30;\n/** Fused candidates checked for permission and ranked; over-fetched so filtering and type caps rarely empty the page. */\nconst FUSED = 60;\n/** The most results a call returns, unless `maxResults` is configured higher; a larger `limit` is clamped. */\nconst MAX_LIMIT = 20;\nconst EXCERPT_CHARS = 1500;\n/** A word in this many chunks or fewer is rare: strong evidence of what the user means. */\nconst RARE_MAX_CHUNKS = 3;\n\nexport interface SearchKnowledgeDeps {\n actionsRegistry: ActionsRegistryService;\n store: KnowledgeStore;\n catalog: CatalogApi;\n auth: AuthService;\n embeddingModel: EmbeddingModel;\n types: Map<string, KnowledgeTypeSettings>;\n maxResults: number;\n /** The ranker in effect: the default, or one a module set. */\n ranker: () => KnowledgeRanker;\n}\n\n/**\n * Registers `search-knowledge`: hybrid retrieval over the knowledge index. Each\n * phrasing is embedded and matched by meaning; the query words are matched\n * exactly (full-text, any word) and, for words absent from the index, by\n * near spelling (trigram). Words found in only a few chunks get a list of\n * their own, so a rare name or acronym buried in a paragraph about something\n * else is not outvoted by sections that are merely near the question.\n *\n * Each list keeps its best chunks of every document type, so a large source\n * cannot fill a list on its own. The lists are fused by reciprocal rank with\n * one vote per method, each chunk's score multiplied by its type's weight:\n * the phrasings share the meaning vote, so extra alternatives broaden recall\n * without drowning out exact matches. Results gated by a catalog entity are\n * filtered to entities the caller can read; the rest are visible to every\n * assistant user, as they are in Backstage search. Repeated text is shown\n * once, and the ranker picks the results from what remains.\n */\nexport function registerSearchKnowledgeAction({\n actionsRegistry,\n store,\n catalog,\n auth,\n embeddingModel,\n types,\n maxResults,\n ranker,\n}: SearchKnowledgeDeps): void {\n actionsRegistry.register({\n name: 'search-knowledge',\n title: 'Search Knowledge',\n attributes: { destructive: false, readOnly: true, idempotent: true },\n description: `Search everything the organization's Backstage search indexes (TechDocs, the software catalog, and any other source it collects, such as Confluence or wiki pages) by meaning, not only by exact words. Best for questions in the user's own words: it finds a page even when the question is worded differently from it (\"fix errored messages\" finds \"Retry dead-letter messages\") and tolerates misspelled names (\"Pilon\" finds \"Pylon\"). It returns the most relevant sections, not every match, and cannot filter by entity kind.\n${sourcesDescription(types)}\n\nHow to search the organization's docs and catalog:\n1. Start here, with the user's question.\n2. If the results don't clearly answer, also run a keyword search: the query tool (catalog and TechDocs at once) when it is available, otherwise search-techdocs and search-catalog. Keyword search finds rare names, acronyms and jargon that ranking by meaning can miss. For a bare name, term or acronym, run both searches from the start.\n3. Report something as missing only after both searches come back empty.\n\nPass the user's question as the \"query\" parameter. Pass up to three \"alternatives\": other phrasings, synonyms, or the internal terms the docs might use.\n\nEach result has a title (its heading path), an excerpt, a location, its source type, the fields the source attached (such as status, space, or last update: use them to judge how current it is), and, for catalog and TechDocs results, the entity ref. A location starting with /docs/ is a TechDocs page: pass it to read-techdocs to read the full page. Any other location is a link to the page or entity; cite it.`,\n schema: {\n input: z =>\n z.object({\n query: z.string().min(1).describe('The question or topic to find'),\n alternatives: z\n .array(z.string())\n .optional()\n .describe(\n 'Up to three other phrasings or terms for the same thing; extra ones are ignored',\n ),\n types: z\n .array(z.string())\n .optional()\n .describe(\n 'Search only these sources (search document types); omit to search the default ones',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .optional()\n .default(maxResults)\n .describe(\n `Number of results to return, at most ${Math.max(\n MAX_LIMIT,\n maxResults,\n )}`,\n ),\n }),\n output: z =>\n z.object({\n results: z.array(\n z.object({\n title: z.string(),\n location: z.string(),\n type: z.string().optional(),\n entityRef: z.string().optional(),\n metadata: z\n .record(\n z.string(),\n z.union([z.string(), z.number(), z.boolean()]),\n )\n .optional(),\n excerpt: z.string(),\n }),\n ),\n }),\n },\n action: async ({ input, credentials }) => {\n if (!(await store.isReady())) {\n throw new Error(\n 'The knowledge index is still being built; meanwhile use keyword search: the query tool when it is available, otherwise search-techdocs and search-catalog.',\n );\n }\n\n const filter = selectTypes(types, await store.types(), input.types);\n const limit = Math.min(input.limit, Math.max(MAX_LIMIT, maxResults));\n const phrasings = phrasingsOf(input.query, input.alternatives);\n const words = queryWords(phrasings);\n const joined = joinedWords(phrasings).filter(w => !words.includes(w));\n const [{ embeddings }, frequencies] = await Promise.all([\n embedMany({ model: embeddingModel, values: phrasings }),\n store.documentFrequencies(\n [...words, ...joined],\n RARE_MAX_CHUNKS + 1,\n filter,\n ),\n ]);\n const known = [...words, ...joined].filter(\n w => (frequencies.get(w) ?? 0) > 0,\n );\n const rare = known.filter(w => frequencies.get(w)! <= RARE_MAX_CHUNKS);\n const unknown = words.filter(w => frequencies.get(w) === 0);\n\n const lists: RankedList[] = await Promise.all([\n ...embeddings.map(async embedding => ({\n name: 'semantic' as const,\n hits: await store.nearest(embedding, CANDIDATES, filter),\n weight: 1 / embeddings.length,\n })),\n store\n .keyword(known, CANDIDATES, filter)\n .then(hits => ({ name: 'keyword' as const, hits, weight: 1 })),\n store\n .keyword(rare, CANDIDATES, filter)\n .then(hits => ({ name: 'rareWord' as const, hits, weight: 1 })),\n store\n .fuzzy(unknown, CANDIDATES, filter)\n .then(hits => ({ name: 'fuzzy' as const, hits, weight: 1 })),\n ]);\n\n const settingsOf = typeSettingsOf(types);\n const fused = fuseRankings(lists, type => settingsOf(type).weight).slice(\n 0,\n FUSED,\n );\n const byId = new Map(\n (await store.chunks(fused.map(hit => hit.id))).map(chunk => [\n chunk.id,\n chunk,\n ]),\n );\n const found = fused.flatMap(hit => {\n const chunk = byId.get(hit.id);\n return chunk ? [{ hit, chunk }] : [];\n });\n\n const readable = await readableEntityRefs(catalog, auth, credentials, [\n ...new Set(\n found.flatMap(({ chunk }) =>\n chunk.entityRef ? [chunk.entityRef] : [],\n ),\n ),\n ]);\n\n const seen = new Set<string>();\n const candidates: KnowledgeCandidate[] = [];\n for (const { hit, chunk } of found) {\n const key = `${chunk.location}|${chunk.title}`;\n if (\n (chunk.entityRef && !readable.has(chunk.entityRef)) ||\n seen.has(key) ||\n seen.has(chunk.content)\n ) {\n continue;\n }\n seen.add(key);\n seen.add(chunk.content);\n candidates.push({ ...chunk, score: hit.score, ranks: hit.ranks });\n }\n\n const ids = new Set(candidates.map(candidate => candidate.id));\n const ranked = await ranker()({\n query: input.query,\n phrasings,\n types: filter.types,\n candidates,\n maxResults: limit,\n });\n const results = ranked\n .filter(candidate => ids.delete(candidate.id))\n .slice(0, limit)\n .map(candidate => ({\n title: candidate.title,\n location: candidate.location,\n ...(candidate.type ? { type: candidate.type } : {}),\n ...(candidate.entityRef ? { entityRef: candidate.entityRef } : {}),\n ...(candidate.metadata ? { metadata: candidate.metadata } : {}),\n excerpt: candidate.content.slice(0, EXCERPT_CHARS),\n }));\n return { output: { results } };\n },\n });\n}\n\n/**\n * The tool description's paragraph on sources: the configured types, which\n * ones are searched only when named, and what each holds.\n */\nexport function sourcesDescription(\n types: Map<string, KnowledgeTypeSettings>,\n): string {\n const listed = [...types].filter(\n ([, settings]) => settings.enabled && settings.weight > 0,\n );\n const lines = listed.map(\n ([type, settings]) =>\n `- ${type}${settings.default ? '' : ' (only when named in \"types\")'}${\n settings.description ? `: ${settings.description}` : ''\n }`,\n );\n return [\n '',\n `Sources: pass \"types\" to search only the named search document types; without it the search covers every source except those marked as searched only when named. An unknown name returns the list of types.`,\n ...lines,\n ].join('\\n');\n}\n\n/**\n * The subset of entity refs the caller can read, asked of the catalog as the\n * caller — so the catalog's own permission policy decides, and a deleted\n * entity's leftover chunks never surface.\n */\nasync function readableEntityRefs(\n catalog: CatalogApi,\n auth: AuthService,\n credentials: BackstageCredentials,\n entityRefs: string[],\n): Promise<Set<string>> {\n if (entityRefs.length === 0) {\n return new Set();\n }\n const { token } = await auth.getPluginRequestToken({\n onBehalfOf: credentials,\n targetPluginId: 'catalog',\n });\n const { items } = await catalog.getEntitiesByRefs(\n { entityRefs, fields: ['kind', 'metadata.namespace', 'metadata.name'] },\n { token },\n );\n return new Set(entityRefs.filter((_, i) => items[i]));\n}\n"],"names":["selectTypes","phrasingsOf","queryWords","joinedWords","embedMany","typeSettingsOf","fuseRankings"],"mappings":"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"}
|
package/dist/rank.cjs.js
CHANGED
|
@@ -7,6 +7,21 @@ const MAX_WORDS = 8;
|
|
|
7
7
|
function tokens(phrasing) {
|
|
8
8
|
return phrasing.toLowerCase().split(/[^\p{L}\p{N}_-]+/u).map((raw) => raw.replace(/^[-_]+|[-_]+$/g, "")).filter(Boolean);
|
|
9
9
|
}
|
|
10
|
+
const MAX_ALTERNATIVES = 3;
|
|
11
|
+
function phrasingsOf(query, alternatives = []) {
|
|
12
|
+
const seen = /* @__PURE__ */ new Set([query.trim().toLowerCase()]);
|
|
13
|
+
const phrasings = [query];
|
|
14
|
+
for (const alternative of alternatives) {
|
|
15
|
+
const trimmed = alternative.trim();
|
|
16
|
+
const key = trimmed.toLowerCase();
|
|
17
|
+
if (phrasings.length > MAX_ALTERNATIVES || !trimmed || seen.has(key)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
seen.add(key);
|
|
21
|
+
phrasings.push(trimmed);
|
|
22
|
+
}
|
|
23
|
+
return phrasings;
|
|
24
|
+
}
|
|
10
25
|
function queryWords(phrasings) {
|
|
11
26
|
const words = /* @__PURE__ */ new Set();
|
|
12
27
|
for (const phrasing of phrasings) {
|
|
@@ -47,7 +62,9 @@ function fuseRankings(lists, typeWeight = () => 1) {
|
|
|
47
62
|
return [...fused.values()].map((hit) => ({ ...hit, score: hit.score * typeWeight(hit.type) })).filter((hit) => hit.score > 0).sort((a, b) => b.score - a.score);
|
|
48
63
|
}
|
|
49
64
|
|
|
65
|
+
exports.MAX_ALTERNATIVES = MAX_ALTERNATIVES;
|
|
50
66
|
exports.fuseRankings = fuseRankings;
|
|
51
67
|
exports.joinedWords = joinedWords;
|
|
68
|
+
exports.phrasingsOf = phrasingsOf;
|
|
52
69
|
exports.queryWords = queryWords;
|
|
53
70
|
//# sourceMappingURL=rank.cjs.js.map
|
package/dist/rank.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rank.cjs.js","sources":["../src/rank.ts"],"sourcesContent":["const STOPWORDS = new Set(\n (\n 'a about an and are as at be but by can do does for from how i if in is it ' +\n 'its me my of on or our should so that the their them then there these ' +\n 'this to us was we what when where which who why will with you your'\n ).split(' '),\n);\n\nconst MAX_WORDS = 8;\n\nfunction tokens(phrasing: string): string[] {\n return phrasing\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}_-]+/u)\n .map(raw => raw.replace(/^[-_]+|[-_]+$/g, ''))\n .filter(Boolean);\n}\n\n/**\n * The distinct content words of the given phrasings: lowercased, at least\n * three characters, stopwords dropped, capped at eight. These drive the\n * keyword and fuzzy lists; the full phrasings drive the vector lists.\n */\nexport function queryWords(phrasings: string[]): string[] {\n const words = new Set<string>();\n for (const phrasing of phrasings) {\n for (const word of tokens(phrasing)) {\n if (word.length >= 3 && !STOPWORDS.has(word)) {\n words.add(word);\n }\n }\n }\n return [...words].slice(0, MAX_WORDS);\n}\n\n/**\n * Adjacent word pairs written as one word (\"tally up\" → \"tallyup\"), capped at\n * eight, for terms the user split in two. Only pairs the index contains are\n * searched, so the nonsense pairs cost nothing.\n */\nexport function joinedWords(phrasings: string[]): string[] {\n const words = new Set<string>();\n for (const phrasing of phrasings) {\n const parts = tokens(phrasing);\n for (let i = 1; i < parts.length; i++) {\n words.add(parts[i - 1] + parts[i]);\n }\n }\n return [...words].slice(0, MAX_WORDS);\n}\n\n/** The retrieval methods whose lists are fused. */\nexport type ListName = 'semantic' | 'keyword' | 'rareWord' | 'fuzzy';\n\n/** A ranked list of chunks from one retrieval method, with its fusion weight. */\nexport interface RankedList {\n name: ListName;\n hits: Array<{ id: string; type?: string }>;\n weight: number;\n}\n\n/** A chunk after fusion. */\nexport interface FusedHit {\n id: string;\n type?: string;\n /** Fused score, after the type's weight. */\n score: number;\n /** The chunk's 1-based rank in each list it appears in; the best one for the semantic lists. */\n ranks: Partial<Record<ListName, number>>;\n}\n\nconst RRF_K = 60;\n\n/**\n * Reciprocal rank fusion: each list contributes `weight / (60 + rank)` per\n * chunk, and the sum is multiplied by the chunk's type weight. Rewards chunks\n * several methods agree on without comparing their raw scores, which live on\n * different scales. Chunks whose type weighs 0 are left out.\n */\nexport function fuseRankings(\n lists: RankedList[],\n typeWeight: (type: string | undefined) => number = () => 1,\n): FusedHit[] {\n const fused = new Map<string, FusedHit>();\n for (const { name, hits, weight } of lists) {\n hits.forEach(({ id, type }, index) => {\n const hit = fused.get(id) ?? {\n id,\n ...(type ? { type } : {}),\n score: 0,\n ranks: {},\n };\n hit.score += weight / (RRF_K + index + 1);\n hit.ranks[name] = Math.min(hit.ranks[name] ?? Infinity, index + 1);\n fused.set(id, hit);\n });\n }\n return [...fused.values()]\n .map(hit => ({ ...hit, score: hit.score * typeWeight(hit.type) }))\n .filter(hit => hit.score > 0)\n .sort((a, b) => b.score - a.score);\n}\n"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"rank.cjs.js","sources":["../src/rank.ts"],"sourcesContent":["const STOPWORDS = new Set(\n (\n 'a about an and are as at be but by can do does for from how i if in is it ' +\n 'its me my of on or our should so that the their them then there these ' +\n 'this to us was we what when where which who why will with you your'\n ).split(' '),\n);\n\nconst MAX_WORDS = 8;\n\nfunction tokens(phrasing: string): string[] {\n return phrasing\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}_-]+/u)\n .map(raw => raw.replace(/^[-_]+|[-_]+$/g, ''))\n .filter(Boolean);\n}\n\n/** Alternatives embedded per search; extra ones are ignored, not an error. */\nexport const MAX_ALTERNATIVES = 3;\n\n/**\n * The query and its first three distinct alternatives, trimmed: blanks and\n * repeats of the query or of each other (ignoring case) are dropped, so no\n * phrasing is embedded twice.\n */\nexport function phrasingsOf(\n query: string,\n alternatives: string[] = [],\n): string[] {\n const seen = new Set([query.trim().toLowerCase()]);\n const phrasings = [query];\n for (const alternative of alternatives) {\n const trimmed = alternative.trim();\n const key = trimmed.toLowerCase();\n if (phrasings.length > MAX_ALTERNATIVES || !trimmed || seen.has(key)) {\n continue;\n }\n seen.add(key);\n phrasings.push(trimmed);\n }\n return phrasings;\n}\n\n/**\n * The distinct content words of the given phrasings: lowercased, at least\n * three characters, stopwords dropped, capped at eight. These drive the\n * keyword and fuzzy lists; the full phrasings drive the vector lists.\n */\nexport function queryWords(phrasings: string[]): string[] {\n const words = new Set<string>();\n for (const phrasing of phrasings) {\n for (const word of tokens(phrasing)) {\n if (word.length >= 3 && !STOPWORDS.has(word)) {\n words.add(word);\n }\n }\n }\n return [...words].slice(0, MAX_WORDS);\n}\n\n/**\n * Adjacent word pairs written as one word (\"tally up\" → \"tallyup\"), capped at\n * eight, for terms the user split in two. Only pairs the index contains are\n * searched, so the nonsense pairs cost nothing.\n */\nexport function joinedWords(phrasings: string[]): string[] {\n const words = new Set<string>();\n for (const phrasing of phrasings) {\n const parts = tokens(phrasing);\n for (let i = 1; i < parts.length; i++) {\n words.add(parts[i - 1] + parts[i]);\n }\n }\n return [...words].slice(0, MAX_WORDS);\n}\n\n/** The retrieval methods whose lists are fused. */\nexport type ListName = 'semantic' | 'keyword' | 'rareWord' | 'fuzzy';\n\n/** A ranked list of chunks from one retrieval method, with its fusion weight. */\nexport interface RankedList {\n name: ListName;\n hits: Array<{ id: string; type?: string }>;\n weight: number;\n}\n\n/** A chunk after fusion. */\nexport interface FusedHit {\n id: string;\n type?: string;\n /** Fused score, after the type's weight. */\n score: number;\n /** The chunk's 1-based rank in each list it appears in; the best one for the semantic lists. */\n ranks: Partial<Record<ListName, number>>;\n}\n\nconst RRF_K = 60;\n\n/**\n * Reciprocal rank fusion: each list contributes `weight / (60 + rank)` per\n * chunk, and the sum is multiplied by the chunk's type weight. Rewards chunks\n * several methods agree on without comparing their raw scores, which live on\n * different scales. Chunks whose type weighs 0 are left out.\n */\nexport function fuseRankings(\n lists: RankedList[],\n typeWeight: (type: string | undefined) => number = () => 1,\n): FusedHit[] {\n const fused = new Map<string, FusedHit>();\n for (const { name, hits, weight } of lists) {\n hits.forEach(({ id, type }, index) => {\n const hit = fused.get(id) ?? {\n id,\n ...(type ? { type } : {}),\n score: 0,\n ranks: {},\n };\n hit.score += weight / (RRF_K + index + 1);\n hit.ranks[name] = Math.min(hit.ranks[name] ?? Infinity, index + 1);\n fused.set(id, hit);\n });\n }\n return [...fused.values()]\n .map(hit => ({ ...hit, score: hit.score * typeWeight(hit.type) }))\n .filter(hit => hit.score > 0)\n .sort((a, b) => b.score - a.score);\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drewswiredin/backstage-plugin-assistants-backend-module-semantic-search",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Knowledge index for AI Assistants for Backstage: semantic, fuzzy, and keyword search in Postgres over everything Backstage search indexes.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|