From 6bdad50c7d15de90bfbe518a2d7d839cb0986d6a Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Fri, 7 Mar 2025 11:22:50 +0000 Subject: [PATCH 01/49] Add support for defining generative config at query-time (dynamic RAG) --- .github/workflows/main.yaml | 12 +- src/collections/deserialize/index.ts | 36 +- src/collections/generate/index.ts | 283 +++++----- src/collections/generate/integration.test.ts | 84 ++- src/collections/generate/types.ts | 186 ++++--- src/collections/query/check.ts | 6 + src/collections/serialize/index.ts | 136 ++++- src/collections/types/generate.ts | 169 +++++- src/grpc/searcher.ts | 1 + src/proto/v1/generative.ts | 519 ++++++++++++++++++- src/utils/dbVersion.ts | 11 + 11 files changed, 1190 insertions(+), 253 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 87c01d56..8968c421 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -8,11 +8,12 @@ on: env: WEAVIATE_124: 1.24.26 - WEAVIATE_125: 1.25.30 - WEAVIATE_126: 1.26.14 - WEAVIATE_127: 1.27.11 - WEAVIATE_128: 1.28.4 + WEAVIATE_125: 1.25.34 + WEAVIATE_126: 1.26.17 + WEAVIATE_127: 1.27.14 + WEAVIATE_128: 1.28.8 WEAVIATE_129: 1.29.0 + WEAVIATE_130: 1.30.0-dev-680e323 jobs: checks: @@ -43,7 +44,8 @@ jobs: { node: "22.x", weaviate: $WEAVIATE_128}, { node: "18.x", weaviate: $WEAVIATE_129}, { node: "20.x", weaviate: $WEAVIATE_129}, - { node: "22.x", weaviate: $WEAVIATE_129} + { node: "22.x", weaviate: $WEAVIATE_129}, + { node: "22.x", weaviate: $WEAVIATE_130} ] steps: - uses: actions/checkout@v3 diff --git a/src/collections/deserialize/index.ts b/src/collections/deserialize/index.ts index 588c2642..41f9beff 100644 --- a/src/collections/deserialize/index.ts +++ b/src/collections/deserialize/index.ts @@ -25,6 +25,8 @@ import { AggregateResult, AggregateText, AggregateType, + GenerativeConfigRuntime, + GenerativeMetadata, PropertiesMetrics, } from '../index.js'; import { referenceFromObjects } from '../references/utils.js'; @@ -207,11 +209,24 @@ export class Deserialize { }; } - public generate(reply: SearchReply): GenerativeReturn { + public generate( + reply: SearchReply + ): GenerativeReturn { return { objects: reply.results.map((result) => { return { - generated: result.metadata?.generativePresent ? result.metadata?.generative : undefined, + generated: result.metadata?.generativePresent + ? result.metadata?.generative + : result.generative + ? result.generative.values[0].result + : undefined, + generative: result.generative + ? { + text: result.generative.values[0].result, + debug: result.generative.values[0].debug, + metadata: result.generative.values[0].metadata as GenerativeMetadata, + } + : undefined, metadata: Deserialize.metadata(result.metadata), properties: this.properties(result.properties), references: this.references(result.properties), @@ -219,7 +234,18 @@ export class Deserialize { vectors: Deserialize.vectors(result.metadata), } as any; }), - generated: reply.generativeGroupedResult, + generated: + reply.generativeGroupedResult !== '' + ? reply.generativeGroupedResult + : reply.generativeGroupedResults + ? reply.generativeGroupedResults.values[0].result + : undefined, + generative: reply.generativeGroupedResults + ? { + text: reply.generativeGroupedResults?.values[0].result, + metadata: reply.generativeGroupedResults?.values[0].metadata as GenerativeMetadata, + } + : undefined, }; } @@ -252,9 +278,9 @@ export class Deserialize { }; } - public generateGroupBy(reply: SearchReply): GenerativeGroupByReturn { + public generateGroupBy(reply: SearchReply): GenerativeGroupByReturn { const objects: GroupByObject[] = []; - const groups: Record> = {}; + const groups: Record> = {}; reply.groupByResults.forEach((result) => { const objs = result.objects.map((object) => { return { diff --git a/src/collections/generate/index.ts b/src/collections/generate/index.ts index 3af6fef1..60de4f94 100644 --- a/src/collections/generate/index.ts +++ b/src/collections/generate/index.ts @@ -5,6 +5,7 @@ import { DbVersionSupport } from '../../utils/dbVersion.js'; import { WeaviateInvalidInputError } from '../../errors.js'; import { toBase64FromMedia } from '../../index.js'; +import { GenerativeSearch } from '../../proto/v1/generative.js'; import { SearchReply } from '../../proto/v1/search_get.js'; import { Deserialize } from '../deserialize/index.js'; import { Check } from '../query/check.js'; @@ -28,6 +29,7 @@ import { Serialize } from '../serialize/index.js'; import { GenerateOptions, GenerateReturn, + GenerativeConfigRuntime, GenerativeGroupByReturn, GenerativeReturn, GroupByOptions, @@ -51,107 +53,118 @@ class GenerateManager implements Generate { return new GenerateManager(new Check(connection, name, dbVersionSupport, consistencyLevel, tenant)); } - private async parseReply(reply: SearchReply) { + private async parseReply(reply: SearchReply) { const deserialize = await Deserialize.use(this.check.dbVersionSupport); - return deserialize.generate(reply); + return deserialize.generate(reply); } - private async parseGroupByReply( + private async parseGroupByReply( opts: SearchOptions | GroupByOptions | undefined, reply: SearchReply ) { const deserialize = await Deserialize.use(this.check.dbVersionSupport); return Serialize.search.isGroupBy(opts) ? deserialize.generateGroupBy(reply) - : deserialize.generate(reply); + : deserialize.generate(reply); } - public fetchObjects( - generate: GenerateOptions, + public fetchObjects( + generate: GenerateOptions, opts?: FetchObjectsOptions - ): Promise> { - return this.check - .fetchObjects(opts) - .then(({ search }) => + ): Promise> { + return Promise.all([this.check.fetchObjects(opts), this.check.supportForSingleGrouped()]) + .then(async ([{ search }, supportsSingleGrouped]) => search.withFetch({ ...Serialize.search.fetchObjects(opts), - generative: Serialize.generative(generate), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), }) ) .then((reply) => this.parseReply(reply)); } - public bm25( + public bm25( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseBm25Options - ): Promise>; - public bm25( + ): Promise>; + public bm25( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByBm25Options - ): Promise>; - public bm25(query: string, generate: GenerateOptions, opts?: Bm25Options): GenerateReturn { - return this.check - .bm25(opts) - .then(({ search }) => + ): Promise>; + public bm25( + query: string, + generate: GenerateOptions, + opts?: Bm25Options + ): GenerateReturn { + return Promise.all([this.check.bm25(opts), this.check.supportForSingleGrouped()]) + .then(async ([{ search }, supportsSingleGrouped]) => search.withBm25({ ...Serialize.search.bm25(query, opts), - generative: Serialize.generative(generate), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public hybrid( + public hybrid( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseHybridOptions - ): Promise>; - public hybrid( + ): Promise>; + public hybrid( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByHybridOptions - ): Promise>; - public hybrid(query: string, generate: GenerateOptions, opts?: HybridOptions): GenerateReturn { - return this.check - .hybridSearch(opts) - .then(({ search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }) => - search.withHybrid({ - ...Serialize.search.hybrid( - { - query, - supportsTargets, - supportsVectorsForTargets, - supportsWeightsForTargets, - }, - opts - ), - generative: Serialize.generative(generate), - }) + ): Promise>; + public hybrid( + query: string, + generate: GenerateOptions, + opts?: HybridOptions + ): GenerateReturn { + return Promise.all([this.check.hybridSearch(opts), this.check.supportForSingleGrouped()]) + .then( + async ([ + { search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }, + supportsSingleGrouped, + ]) => + search.withHybrid({ + ...Serialize.search.hybrid( + { + query, + supportsTargets, + supportsVectorsForTargets, + supportsWeightsForTargets, + }, + opts + ), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), + }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearImage( + public nearImage( image: string | Buffer, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; - public nearImage( + ): Promise>; + public nearImage( image: string | Buffer, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; - public nearImage( + ): Promise>; + public nearImage( image: string | Buffer, - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn { - return this.check - .nearSearch(opts) - .then(({ search, supportsTargets, supportsWeightsForTargets }) => - toBase64FromMedia(image).then((image) => + ): GenerateReturn { + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGrouped()]) + .then(([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => + Promise.all([ + toBase64FromMedia(image), + Serialize.generative({ supportsSingleGrouped }, generate), + ]).then(([image, generative]) => search.withNearImage({ ...Serialize.search.nearImage( { @@ -161,27 +174,30 @@ class GenerateManager implements Generate { }, opts ), - generative: Serialize.generative(generate), + generative, }) ) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearObject( + public nearObject( id: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; - public nearObject( + ): Promise>; + public nearObject( id: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; - public nearObject(id: string, generate: GenerateOptions, opts?: NearOptions): GenerateReturn { - return this.check - .nearSearch(opts) - .then(({ search, supportsTargets, supportsWeightsForTargets }) => + ): Promise>; + public nearObject( + id: string, + generate: GenerateOptions, + opts?: NearOptions + ): GenerateReturn { + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGrouped()]) + .then(async ([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => search.withNearObject({ ...Serialize.search.nearObject( { @@ -191,30 +207,29 @@ class GenerateManager implements Generate { }, opts ), - generative: Serialize.generative(generate), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearText( + public nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearTextOptions - ): Promise>; - public nearText( + ): Promise>; + public nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearTextOptions - ): Promise>; - public nearText( + ): Promise>; + public nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn { - return this.check - .nearSearch(opts) - .then(({ search, supportsTargets, supportsWeightsForTargets }) => + ): GenerateReturn { + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGrouped()]) + .then(async ([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => search.withNearText({ ...Serialize.search.nearText( { @@ -224,114 +239,124 @@ class GenerateManager implements Generate { }, opts ), - generative: Serialize.generative(generate), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearVector( + public nearVector( vector: number[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; - public nearVector( + ): Promise>; + public nearVector( vector: number[], - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; - public nearVector( + ): Promise>; + public nearVector( vector: number[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn { - return this.check - .nearVector(vector, opts) - .then(({ search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }) => - search.withNearVector({ - ...Serialize.search.nearVector( - { - vector, - supportsTargets, - supportsVectorsForTargets, - supportsWeightsForTargets, - }, - opts - ), - generative: Serialize.generative(generate), - }) + ): GenerateReturn { + return Promise.all([this.check.nearVector(vector, opts), this.check.supportForSingleGrouped()]) + .then( + async ([ + { search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }, + supportsSingleGrouped, + ]) => + search.withNearVector({ + ...Serialize.search.nearVector( + { + vector, + supportsTargets, + supportsVectorsForTargets, + supportsWeightsForTargets, + }, + opts + ), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), + }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearMedia( + public nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; - public nearMedia( + ): Promise>; + public nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; - public nearMedia( + ): Promise>; + public nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn { - return this.check - .nearSearch(opts) - .then(({ search, supportsTargets, supportsWeightsForTargets }) => { + ): GenerateReturn { + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGrouped()]) + .then(([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => { const args = { supportsTargets, supportsWeightsForTargets, }; - const generative = Serialize.generative(generate); - let send: (media: string) => Promise; + let send: (media: string, generative: GenerativeSearch) => Promise; switch (type) { case 'audio': - send = (media) => + send = (media, generative) => search.withNearAudio({ ...Serialize.search.nearAudio({ audio: media, ...args }, opts), generative, }); break; case 'depth': - send = (media) => + send = (media, generative) => search.withNearDepth({ ...Serialize.search.nearDepth({ depth: media, ...args }, opts), generative, }); break; case 'image': - send = (media) => + send = (media, generative) => search.withNearImage({ ...Serialize.search.nearImage({ image: media, ...args }, opts), generative, }); break; case 'imu': - send = (media) => - search.withNearIMU({ ...Serialize.search.nearIMU({ imu: media, ...args }, opts), generative }); + send = (media, generative) => + search.withNearIMU({ + ...Serialize.search.nearIMU({ imu: media, ...args }, opts), + generative, + }); break; case 'thermal': - send = (media) => + send = (media, generative) => search.withNearThermal({ ...Serialize.search.nearThermal({ thermal: media, ...args }, opts), generative, }); break; case 'video': - send = (media) => - search.withNearVideo({ ...Serialize.search.nearVideo({ video: media, ...args }), generative }); + send = (media, generative) => + search.withNearVideo({ + ...Serialize.search.nearVideo({ video: media, ...args }), + generative, + }); break; default: throw new WeaviateInvalidInputError(`Invalid media type: ${type}`); } - return toBase64FromMedia(media).then(send); + return Promise.all([ + toBase64FromMedia(media), + Serialize.generative({ supportsSingleGrouped }, generate), + ]).then(([media, generative]) => send(media, generative)); }) .then((reply) => this.parseGroupByReply(opts, reply)); } diff --git a/src/collections/generate/integration.test.ts b/src/collections/generate/integration.test.ts index 1be98451..5e478093 100644 --- a/src/collections/generate/integration.test.ts +++ b/src/collections/generate/integration.test.ts @@ -27,10 +27,10 @@ maybe('Testing of the collection.generate methods with a simple collection', () testProp: string; }; - const generateOpts: GenerateOptions = { + const generateOpts = { singlePrompt: 'Write a haiku about ducks for {testProp}', groupedTask: 'What is the value of testProp here?', - groupedProperties: ['testProp'], + groupedProperties: ['testProp'] as 'testProp'[], }; afterAll(() => { @@ -162,7 +162,7 @@ maybe('Testing of the groupBy collection.generate methods with a simple collecti testProp: string; }; - const generateOpts: GenerateOptions = { + const generateOpts: GenerateOptions = { singlePrompt: 'Write a haiku about ducks for {testProp}', groupedTask: 'What is the value of testProp here?', groupedProperties: ['testProp'], @@ -421,3 +421,81 @@ maybe('Testing of the collection.generate methods with a multi vector collection expect(ret.objects[1].generated).toBeDefined(); }); }); + +maybe('Testing of the collection.generate methods with runtime generative config', () => { + let client: WeaviateClient; + let collection: Collection; + const collectionName = 'TestCollectionGenerateConfigRuntime'; + + type TestCollectionGenerateConfigRuntime = { + testProp: string; + }; + + afterAll(() => { + return client.collections.delete(collectionName).catch((err) => { + console.error(err); + throw err; + }); + }); + + beforeAll(async () => { + client = await makeOpenAIClient(); + collection = client.collections.get(collectionName); + return client.collections + .create({ + name: collectionName, + properties: [ + { + name: 'testProp', + dataType: 'text', + }, + ], + }) + .then(() => { + return collection.data.insert({ + properties: { + testProp: 'test', + }, + }); + }); + }); + + it('should generate using a runtime config without search', async () => { + const query = () => + collection.generate.fetchObjects({ + singlePrompt: { + prompt: 'Write a haiku about ducks for {testProp}', + debug: true, + metadata: true, + }, + groupedTask: { + prompt: 'What is the value of testProp here?', + nonBlobProperties: ['testProp'], + metadata: true, + }, + config: { + name: 'generative-openai', + config: { + model: 'gpt-4o-mini', + }, + }, + }); + + if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 30, 0))) { + await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); + return; + } + + const res = await query(); + expect(res.objects.length).toEqual(1); + expect(res.generated).toBeDefined(); + expect(res.generative?.text).toBeDefined(); + expect(res.generative?.metadata).toBeDefined(); + res.objects.forEach((obj) => { + expect(obj.generated).toBeDefined(); + expect(obj.generative?.text).toBeDefined(); + expect(obj.generative?.metadata).toBeDefined(); + expect(obj.generative?.debug).toBeDefined(); + }); + }); +}); diff --git a/src/collections/generate/types.ts b/src/collections/generate/types.ts index b211a46a..27548bfb 100644 --- a/src/collections/generate/types.ts +++ b/src/collections/generate/types.ts @@ -18,6 +18,7 @@ import { import { GenerateOptions, GenerateReturn, + GenerativeConfigRuntime, GenerativeGroupByReturn, GenerativeReturn, } from '../types/index.js'; @@ -31,11 +32,15 @@ interface Bm25 { * This overload is for performing a search without the `groupBy` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseBm25Options} [opts] - The available options for performing the BM25 search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - bm25(query: string, generate: GenerateOptions, opts?: BaseBm25Options): Promise>; + bm25( + query: string, + generate: GenerateOptions, + opts?: BaseBm25Options + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a keyword-based BM25 search of objects in this collection. * @@ -44,15 +49,15 @@ interface Bm25 { * This overload is for performing a search with the `groupBy` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByBm25Options} opts - The available options for performing the BM25 search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - bm25( + bm25( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByBm25Options - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a keyword-based BM25 search of objects in this collection. * @@ -61,11 +66,15 @@ interface Bm25 { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {Bm25Options} [opts] - The available options for performing the BM25 search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - bm25(query: string, generate: GenerateOptions, opts?: Bm25Options): GenerateReturn; + bm25( + query: string, + generate: GenerateOptions, + opts?: Bm25Options + ): GenerateReturn; } interface Hybrid { @@ -77,15 +86,15 @@ interface Hybrid { * This overload is for performing a search without the `groupBy` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseHybridOptions} [opts] - The available options for performing the hybrid search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - hybrid( + hybrid( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseHybridOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of an object search in this collection using the hybrid algorithm blending keyword-based BM25 and vector-based similarity. * @@ -94,15 +103,15 @@ interface Hybrid { * This overload is for performing a search with the `groupBy` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByHybridOptions} opts - The available options for performing the hybrid search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - hybrid( + hybrid( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByHybridOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of an object search in this collection using the hybrid algorithm blending keyword-based BM25 and vector-based similarity. * @@ -111,11 +120,15 @@ interface Hybrid { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {HybridOptions} [opts] - The available options for performing the hybrid search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - hybrid(query: string, generate: GenerateOptions, opts?: HybridOptions): GenerateReturn; + hybrid( + query: string, + generate: GenerateOptions, + opts?: HybridOptions + ): GenerateReturn; } interface NearMedia { @@ -130,16 +143,16 @@ interface NearMedia { * * @param {string | Buffer} media - The media file to search on. This can be a base64 string, a file path string, or a buffer. * @param {NearMediaType} type - The type of media to search on. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseNearOptions} [opts] - The available options for performing the near-media search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - nearMedia( + nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-audio object search in this collection using an audio-capable vectorization module and vector-based similarity search. * @@ -151,16 +164,16 @@ interface NearMedia { * * @param {string | Buffer} media - The media file to search on. This can be a base64 string, a file path string, or a buffer. * @param {NearMediaType} type - The type of media to search on. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByNearOptions} opts - The available options for performing the near-media search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - nearMedia( + nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-audio object search in this collection using an audio-capable vectorization module and vector-based similarity search. * @@ -172,16 +185,16 @@ interface NearMedia { * * @param {string | Buffer} media - The media to search on. This can be a base64 string, a file path string, or a buffer. * @param {NearMediaType} type - The type of media to search on. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {NearOptions} [opts] - The available options for performing the near-media search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - nearMedia( + nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn; + ): GenerateReturn; } interface NearObject { @@ -193,15 +206,15 @@ interface NearObject { * This overload is for performing a search without the `groupBy` param. * * @param {string} id - The ID of the object to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseNearOptions} [opts] - The available options for performing the near-object search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - nearObject( + nearObject( id: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-object object search in this collection using a vector-based similarity search. * @@ -210,15 +223,15 @@ interface NearObject { * This overload is for performing a search with the `groupBy` param. * * @param {string} id - The ID of the object to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByNearOptions} opts - The available options for performing the near-object search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - nearObject( + nearObject( id: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-object object search in this collection using a vector-based similarity search. * @@ -227,11 +240,15 @@ interface NearObject { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {string} id - The ID of the object to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {NearOptions} [opts] - The available options for performing the near-object search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - nearObject(id: string, generate: GenerateOptions, opts?: NearOptions): GenerateReturn; + nearObject( + id: string, + generate: GenerateOptions, + opts?: NearOptions + ): GenerateReturn; } interface NearText { @@ -245,15 +262,15 @@ interface NearText { * This overload is for performing a search without the `groupBy` param. * * @param {string | string[]} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseNearTextOptions} [opts] - The available options for performing the near-text search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - nearText( + nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearTextOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-image object search in this collection using the image-capable vectorization module and vector-based similarity search. * @@ -264,15 +281,15 @@ interface NearText { * This overload is for performing a search with the `groupBy` param. * * @param {string | string[]} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByNearTextOptions} opts - The available options for performing the near-text search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - nearText( + nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearTextOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-image object search in this collection using the image-capable vectorization module and vector-based similarity search. * @@ -283,15 +300,15 @@ interface NearText { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {string | string[]} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {NearTextOptions} [opts] - The available options for performing the near-text search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - nearText( + nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearTextOptions - ): GenerateReturn; + ): GenerateReturn; } interface NearVector { @@ -303,15 +320,15 @@ interface NearVector { * This overload is for performing a search without the `groupBy` param. * * @param {NearVectorInputType} vector - The vector(s) to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseNearOptions} [opts] - The available options for performing the near-vector search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - nearVector( + nearVector( vector: NearVectorInputType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-vector object search in this collection using vector-based similarity search. * @@ -320,15 +337,15 @@ interface NearVector { * This overload is for performing a search with the `groupBy` param. * * @param {NearVectorInputType} vector - The vector(s) to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByNearOptions} opts - The available options for performing the near-vector search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - nearVector( + nearVector( vector: NearVectorInputType, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-vector object search in this collection using vector-based similarity search. * @@ -337,15 +354,15 @@ interface NearVector { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {NearVectorInputType} vector - The vector(s) to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {NearOptions} [opts] - The available options for performing the near-vector search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - nearVector( + nearVector( vector: NearVectorInputType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn; + ): GenerateReturn; } export interface Generate @@ -355,5 +372,8 @@ export interface Generate NearObject, NearText, NearVector { - fetchObjects: (generate: GenerateOptions, opts?: FetchObjectsOptions) => Promise>; + fetchObjects: ( + generate: GenerateOptions, + opts?: FetchObjectsOptions + ) => Promise>; } diff --git a/src/collections/query/check.ts b/src/collections/query/check.ts index 291738de..81084f66 100644 --- a/src/collections/query/check.ts +++ b/src/collections/query/check.ts @@ -98,6 +98,12 @@ export class Check { return check.supports; }; + public supportForSingleGrouped = async () => { + const check = await this.dbVersionSupport.supportsSingleGrouped(); + if (!check.supports) throw new WeaviateUnsupportedFeatureError(check.message); + return check.supports; + }; + public nearSearch = (opts?: BaseNearOptions) => { return Promise.all([ this.getSearcher(), diff --git a/src/collections/serialize/index.ts b/src/collections/serialize/index.ts index 747bf00a..6ca6585c 100644 --- a/src/collections/serialize/index.ts +++ b/src/collections/serialize/index.ts @@ -25,7 +25,12 @@ import { BatchObject_Properties, BatchObject_SingleTargetRefProps, } from '../../proto/v1/batch.js'; -import { GenerativeSearch } from '../../proto/v1/generative.js'; +import { + GenerativeProvider, + GenerativeSearch, + GenerativeSearch_Grouped, + GenerativeSearch_Single, +} from '../../proto/v1/generative.js'; import { GroupBy, MetadataRequest, @@ -63,6 +68,7 @@ import { SearchNearVectorArgs, SearchNearVideoArgs, } from '../../grpc/searcher.js'; +import { toBase64FromMedia } from '../../index.js'; import { AggregateRequest_Aggregation, AggregateRequest_Aggregation_Boolean, @@ -82,6 +88,7 @@ import { ObjectArrayProperties, ObjectProperties, ObjectPropertiesValue, + TextArray, TextArrayProperties, Vectors as VectorsGrpc, } from '../../proto/v1/base.js'; @@ -97,10 +104,13 @@ import { AggregateBaseOptions, AggregateHybridOptions, AggregateNearOptions, + GenerativeConfigRuntime, GroupByAggregate, + GroupedTask, MultiTargetVectorJoin, PrimitiveKeys, PropertiesMetrics, + SinglePrompt, } from '../index.js'; import { BaseHybridOptions, @@ -818,14 +828,126 @@ export class Serialize { return vec !== undefined && !Array.isArray(vec) && Object.values(vec).some(ArrayInputGuards.is2DArray); }; - public static generative = (generative?: GenerateOptions): GenerativeSearch => { - return GenerativeSearch.fromPartial({ - singleResponsePrompt: generative?.singlePrompt, - groupedResponseTask: generative?.groupedTask, - groupedProperties: generative?.groupedProperties as string[], - }); + private static generativeQuery = async ( + generative: GenerativeConfigRuntime, + opts?: { metadata?: boolean; images?: (string | Buffer)[]; imageProperties?: string[] } + ): Promise => { + const withImages = async >( + config: T, + imgs?: (string | Buffer)[], + imgProps?: string[] + ): Promise => { + if (imgs == undefined && imgProps == undefined) { + return config; + } + return { + ...config, + images: TextArray.fromPartial({ + values: imgs ? await Promise.all(imgs.map(toBase64FromMedia)) : undefined, + }), + imageProperties: TextArray.fromPartial({ values: imgProps }), + }; + }; + + const provider = GenerativeProvider.fromPartial({ returnMetadata: opts?.metadata }); + switch (generative.name) { + case 'generative-anthropic': + provider.anthropic = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + case 'generative-anyscale': + provider.anyscale = generative.config; + break; + case 'generative-aws': + provider.aws = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + case 'generative-cohere': + provider.cohere = generative.config; + break; + case 'generative-databricks': + provider.databricks = generative.config; + break; + case 'generative-dummy': + provider.dummy = generative.config; + break; + case 'generative-friendliai': + provider.friendliai = generative.config; + break; + case 'generative-google': + provider.google = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + case 'generative-mistral': + provider.mistral = generative.config; + break; + case 'generative-nvidia': + provider.nvidia = generative.config; + break; + case 'generative-ollama': + provider.ollama = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + case 'generative-openai': + provider.openai = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + } + return provider; + }; + + public static generative = async ( + args: { supportsSingleGrouped: boolean }, + opts?: GenerateOptions + ): Promise => { + const singlePrompt = Serialize.isSinglePrompt(opts?.singlePrompt) + ? opts.singlePrompt.prompt + : opts?.singlePrompt; + const singlePromptDebug = Serialize.isSinglePrompt(opts?.singlePrompt) + ? opts.singlePrompt.debug + : undefined; + + const groupedTask = Serialize.isGroupedTask(opts?.groupedTask) + ? opts.groupedTask.prompt + : opts?.groupedTask; + const groupedProperties = Serialize.isGroupedTask(opts?.groupedTask) + ? opts.groupedTask.nonBlobProperties + : opts?.groupedProperties; + + const singleOpts = Serialize.isSinglePrompt(opts?.singlePrompt) ? opts.singlePrompt : undefined; + const groupedOpts = Serialize.isGroupedTask(opts?.groupedTask) ? opts.groupedTask : undefined; + + return args.supportsSingleGrouped + ? GenerativeSearch.fromPartial({ + single: opts?.singlePrompt + ? GenerativeSearch_Single.fromPartial({ + prompt: singlePrompt, + debug: singlePromptDebug, + queries: opts.config ? [await Serialize.generativeQuery(opts.config, singleOpts)] : undefined, + }) + : undefined, + grouped: opts?.groupedTask + ? GenerativeSearch_Grouped.fromPartial({ + task: groupedTask, + queries: opts.config + ? [await Serialize.generativeQuery(opts.config, groupedOpts)] + : undefined, + properties: groupedProperties + ? TextArray.fromPartial({ values: groupedProperties as string[] }) + : undefined, + }) + : undefined, + }) + : GenerativeSearch.fromPartial({ + singleResponsePrompt: singlePrompt, + groupedResponseTask: groupedTask, + groupedProperties: groupedProperties as string[], + }); }; + public static isSinglePrompt(arg?: string | SinglePrompt): arg is SinglePrompt { + return typeof arg !== 'string' && arg !== undefined && arg.prompt !== undefined; + } + + public static isGroupedTask(arg?: string | GroupedTask): arg is GroupedTask { + return typeof arg !== 'string' && arg !== undefined && arg.prompt !== undefined; + } + private static bm25QueryProperties = ( properties?: (PrimitiveKeys | Bm25QueryProperty)[] ): string[] | undefined => { diff --git a/src/collections/types/generate.ts b/src/collections/types/generate.ts index b3f6bac2..31eca2e2 100644 --- a/src/collections/types/generate.ts +++ b/src/collections/types/generate.ts @@ -1,54 +1,183 @@ +import { + GenerativeAWS as GenerativeAWSGRPC, + GenerativeAWSMetadata, + GenerativeAnthropic as GenerativeAnthropicGRPC, + GenerativeAnthropicMetadata, + GenerativeAnyscale as GenerativeAnyscaleGRPC, + GenerativeAnyscaleMetadata, + GenerativeCohere as GenerativeCohereGRPC, + GenerativeCohereMetadata, + GenerativeDatabricks as GenerativeDatabricksGRPC, + GenerativeDatabricksMetadata, + GenerativeDebug, + GenerativeDummy as GenerativeDummyGRPC, + GenerativeDummyMetadata, + GenerativeFriendliAI as GenerativeFriendliAIGRPC, + GenerativeFriendliAIMetadata, + GenerativeGoogle as GenerativeGoogleGRPC, + GenerativeGoogleMetadata, + GenerativeMistral as GenerativeMistralGRPC, + GenerativeMistralMetadata, + GenerativeNvidia as GenerativeNvidiaGRPC, + GenerativeNvidiaMetadata, + GenerativeOllama as GenerativeOllamaGRPC, + GenerativeOllamaMetadata, + GenerativeOpenAI as GenerativeOpenAIGRPC, + GenerativeOpenAIMetadata, +} from '../../proto/v1/generative.js'; +import { ModuleConfig } from '../index.js'; import { GroupByObject, GroupByResult, WeaviateGenericObject, WeaviateNonGenericObject } from './query.js'; -export type GenerativeGenericObject = WeaviateGenericObject & { - /** The LLM-generated output applicable to this single object. */ +export type GenerativeGenericObject< + T, + C extends GenerativeConfigRuntime | undefined +> = WeaviateGenericObject & { + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this single object. */ generated?: string; + /** Generative data returned from the LLM inference on this object. */ + generative?: GenerativeSingle; }; -export type GenerativeNonGenericObject = WeaviateNonGenericObject & { - /** The LLM-generated output applicable to this single object. */ - generated?: string; -}; +export type GenerativeNonGenericObject = + WeaviateNonGenericObject & { + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this single object. */ + generated?: string; + /** Generative data returned from the LLM inference on this object. */ + generative?: GenerativeSingle; + }; /** An object belonging to a collection as returned by the methods in the `collection.generate` namespace. * * Depending on the generic type `T`, the object will have subfields that map from `T`'s specific type definition. * If not, then the object will be non-generic and have a `properties` field that maps from a generic string to a `WeaviateField`. */ -export type GenerativeObject = T extends undefined - ? GenerativeNonGenericObject - : GenerativeGenericObject; +export type GenerativeObject = T extends undefined + ? GenerativeNonGenericObject + : GenerativeGenericObject; + +export type GenerativeSingle = { + debug?: GenerativeDebug; + metadata?: GenerativeMetadata; + text?: string; +}; + +export type GenerativeGrouped = { + metadata?: GenerativeMetadata; + text?: string; +}; /** The return of a query method in the `collection.generate` namespace. */ -export type GenerativeReturn = { +export type GenerativeReturn = { /** The objects that were found by the query. */ - objects: GenerativeObject[]; - /** The LLM-generated output applicable to this query as a whole. */ + objects: GenerativeObject[]; + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this query as a whole. */ generated?: string; + generative?: GenerativeGrouped; }; -export type GenerativeGroupByResult = GroupByResult & { +export type GenerativeGroupByResult = GroupByResult & { + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this query as a whole. */ generated?: string; + generative?: GenerativeSingle; }; /** The return of a query method in the `collection.generate` namespace where the `groupBy` argument was specified. */ -export type GenerativeGroupByReturn = { +export type GenerativeGroupByReturn = { /** The objects that were found by the query. */ objects: GroupByObject[]; /** The groups that were created by the query. */ - groups: Record>; - /** The LLM-generated output applicable to this query as a whole. */ + groups: Record>; + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this query as a whole. */ generated?: string; + generative?: GenerativeGrouped; }; /** Options available when defining queries using methods in the `collection.generate` namespace. */ -export type GenerateOptions = { +export type GenerateOptions = { /** The prompt to use when generating content relevant to each object of the collection individually. */ - singlePrompt?: string; + singlePrompt?: string | SinglePrompt; /** The prompt to use when generating content relevant to objects returned by the query as a whole. */ - groupedTask?: string; + groupedTask?: string | GroupedTask; /** The properties to use as context to be injected into the `groupedTask` prompt when performing the grouped generation. */ groupedProperties?: T extends undefined ? string[] : (keyof T)[]; + config?: C; +}; + +export type SinglePrompt = { + prompt: string; + debug?: boolean; + metadata?: boolean; + images?: (string | Buffer)[]; + imageProperties?: string[]; +}; + +export type GroupedTask = { + prompt: string; + metadata?: boolean; + nonBlobProperties?: T extends undefined ? string[] : (keyof T)[]; + images?: (string | Buffer)[]; + imageProperties?: string[]; }; -export type GenerateReturn = Promise> | Promise>; +export type GenerativeConfigRuntime = + | ModuleConfig<'generative-anthropic', GenerativeAnthropicConfigRuntime> + | ModuleConfig<'generative-anyscale', GenerativeAnyscaleConfigRuntime> + | ModuleConfig<'generative-aws', GenerativeAWSConfigRuntime> + | ModuleConfig<'generative-cohere', GenerativeCohereConfigRuntime> + | ModuleConfig<'generative-databricks', GenerativeDatabricksConfigRuntime> + | ModuleConfig<'generative-dummy', GenerativeDummyConfigRuntime> + | ModuleConfig<'generative-friendliai', GenerativeFriendliAIConfigRuntime> + | ModuleConfig<'generative-google', GenerativeGoogleConfigRuntime> + | ModuleConfig<'generative-mistral', GenerativeMistralConfigRuntime> + | ModuleConfig<'generative-nvidia', GenerativeNvidiaConfigRuntime> + | ModuleConfig<'generative-ollama', GenerativeOllamaConfigRuntime> + | ModuleConfig<'generative-openai', GenerativeOpenAIConfigRuntime>; + +export type GenerativeMetadata = C extends undefined + ? never + : C extends infer R extends GenerativeConfigRuntime + ? R['name'] extends 'generative-anthropic' + ? GenerativeAnthropicMetadata + : R['name'] extends 'generative-anyscale' + ? GenerativeAnyscaleMetadata + : R['name'] extends 'generative-aws' + ? GenerativeAWSMetadata + : R['name'] extends 'generative-cohere' + ? GenerativeCohereMetadata + : R['name'] extends 'generative-databricks' + ? GenerativeDatabricksMetadata + : R['name'] extends 'generative-dummy' + ? GenerativeDummyMetadata + : R['name'] extends 'generative-friendliai' + ? GenerativeFriendliAIMetadata + : R['name'] extends 'generative-google' + ? GenerativeGoogleMetadata + : R['name'] extends 'generative-mistral' + ? GenerativeMistralMetadata + : R['name'] extends 'generative-nvidia' + ? GenerativeNvidiaMetadata + : R['name'] extends 'generative-ollama' + ? GenerativeOllamaMetadata + : R['name'] extends 'generative-openai' + ? GenerativeOpenAIMetadata + : never + : never; + +export type GenerateReturn = + | Promise> + | Promise>; + +type omitFields = 'images' | 'imageProperties'; + +export type GenerativeAnthropicConfigRuntime = Omit; +export type GenerativeAnyscaleConfigRuntime = Omit; +export type GenerativeAWSConfigRuntime = Omit; +export type GenerativeCohereConfigRuntime = Omit; +export type GenerativeDatabricksConfigRuntime = Omit; +export type GenerativeDummyConfigRuntime = Omit; +export type GenerativeFriendliAIConfigRuntime = Omit; +export type GenerativeGoogleConfigRuntime = Omit; +export type GenerativeMistralConfigRuntime = Omit; +export type GenerativeNvidiaConfigRuntime = Omit; +export type GenerativeOllamaConfigRuntime = Omit; +export type GenerativeOpenAIConfigRuntime = Omit; diff --git a/src/grpc/searcher.ts b/src/grpc/searcher.ts index 6dc6103e..20496c6f 100644 --- a/src/grpc/searcher.ts +++ b/src/grpc/searcher.ts @@ -171,6 +171,7 @@ export default class Searcher extends Base implements Search { tenant: this.tenant, uses123Api: true, uses125Api: true, + uses127Api: true, }, { metadata: this.metadata, diff --git a/src/proto/v1/generative.ts b/src/proto/v1/generative.ts index 12b1619f..2abae4ba 100644 --- a/src/proto/v1/generative.ts +++ b/src/proto/v1/generative.ts @@ -51,6 +51,7 @@ export interface GenerativeProvider { google?: GenerativeGoogle | undefined; databricks?: GenerativeDatabricks | undefined; friendliai?: GenerativeFriendliAI | undefined; + nvidia?: GenerativeNvidia | undefined; } export interface GenerativeAnthropic { @@ -61,6 +62,8 @@ export interface GenerativeAnthropic { topK?: number | undefined; topP?: number | undefined; stopSequences?: TextArray | undefined; + images?: TextArray | undefined; + imageProperties?: TextArray | undefined; } export interface GenerativeAnyscale { @@ -77,6 +80,8 @@ export interface GenerativeAWS { endpoint?: string | undefined; targetModel?: string | undefined; targetVariant?: string | undefined; + images?: TextArray | undefined; + imageProperties?: TextArray | undefined; } export interface GenerativeCohere { @@ -106,6 +111,8 @@ export interface GenerativeOllama { apiEndpoint?: string | undefined; model?: string | undefined; temperature?: number | undefined; + images?: TextArray | undefined; + imageProperties?: TextArray | undefined; } export interface GenerativeOpenAI { @@ -122,6 +129,8 @@ export interface GenerativeOpenAI { resourceName?: string | undefined; deploymentId?: string | undefined; isAzure?: boolean | undefined; + images?: TextArray | undefined; + imageProperties?: TextArray | undefined; } export interface GenerativeGoogle { @@ -137,6 +146,8 @@ export interface GenerativeGoogle { projectId?: string | undefined; endpointId?: string | undefined; region?: string | undefined; + images?: TextArray | undefined; + imageProperties?: TextArray | undefined; } export interface GenerativeDatabricks { @@ -162,6 +173,14 @@ export interface GenerativeFriendliAI { topP?: number | undefined; } +export interface GenerativeNvidia { + baseUrl?: string | undefined; + model?: string | undefined; + temperature?: number | undefined; + topP?: number | undefined; + maxTokens?: number | undefined; +} + export interface GenerativeAnthropicMetadata { usage: GenerativeAnthropicMetadata_Usage | undefined; } @@ -273,6 +292,16 @@ export interface GenerativeFriendliAIMetadata_Usage { totalTokens?: number | undefined; } +export interface GenerativeNvidiaMetadata { + usage?: GenerativeNvidiaMetadata_Usage | undefined; +} + +export interface GenerativeNvidiaMetadata_Usage { + promptTokens?: number | undefined; + completionTokens?: number | undefined; + totalTokens?: number | undefined; +} + export interface GenerativeMetadata { anthropic?: GenerativeAnthropicMetadata | undefined; anyscale?: GenerativeAnyscaleMetadata | undefined; @@ -285,6 +314,7 @@ export interface GenerativeMetadata { google?: GenerativeGoogleMetadata | undefined; databricks?: GenerativeDatabricksMetadata | undefined; friendliai?: GenerativeFriendliAIMetadata | undefined; + nvidia?: GenerativeNvidiaMetadata | undefined; } export interface GenerativeReply { @@ -630,6 +660,7 @@ function createBaseGenerativeProvider(): GenerativeProvider { google: undefined, databricks: undefined, friendliai: undefined, + nvidia: undefined, }; } @@ -671,6 +702,9 @@ export const GenerativeProvider = { if (message.friendliai !== undefined) { GenerativeFriendliAI.encode(message.friendliai, writer.uint32(98).fork()).ldelim(); } + if (message.nvidia !== undefined) { + GenerativeNvidia.encode(message.nvidia, writer.uint32(106).fork()).ldelim(); + } return writer; }, @@ -765,6 +799,13 @@ export const GenerativeProvider = { message.friendliai = GenerativeFriendliAI.decode(reader, reader.uint32()); continue; + case 13: + if (tag !== 106) { + break; + } + + message.nvidia = GenerativeNvidia.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -788,6 +829,7 @@ export const GenerativeProvider = { google: isSet(object.google) ? GenerativeGoogle.fromJSON(object.google) : undefined, databricks: isSet(object.databricks) ? GenerativeDatabricks.fromJSON(object.databricks) : undefined, friendliai: isSet(object.friendliai) ? GenerativeFriendliAI.fromJSON(object.friendliai) : undefined, + nvidia: isSet(object.nvidia) ? GenerativeNvidia.fromJSON(object.nvidia) : undefined, }; }, @@ -829,6 +871,9 @@ export const GenerativeProvider = { if (message.friendliai !== undefined) { obj.friendliai = GenerativeFriendliAI.toJSON(message.friendliai); } + if (message.nvidia !== undefined) { + obj.nvidia = GenerativeNvidia.toJSON(message.nvidia); + } return obj; }, @@ -869,6 +914,9 @@ export const GenerativeProvider = { message.friendliai = (object.friendliai !== undefined && object.friendliai !== null) ? GenerativeFriendliAI.fromPartial(object.friendliai) : undefined; + message.nvidia = (object.nvidia !== undefined && object.nvidia !== null) + ? GenerativeNvidia.fromPartial(object.nvidia) + : undefined; return message; }, }; @@ -882,6 +930,8 @@ function createBaseGenerativeAnthropic(): GenerativeAnthropic { topK: undefined, topP: undefined, stopSequences: undefined, + images: undefined, + imageProperties: undefined, }; } @@ -908,6 +958,12 @@ export const GenerativeAnthropic = { if (message.stopSequences !== undefined) { TextArray.encode(message.stopSequences, writer.uint32(58).fork()).ldelim(); } + if (message.images !== undefined) { + TextArray.encode(message.images, writer.uint32(66).fork()).ldelim(); + } + if (message.imageProperties !== undefined) { + TextArray.encode(message.imageProperties, writer.uint32(74).fork()).ldelim(); + } return writer; }, @@ -967,6 +1023,20 @@ export const GenerativeAnthropic = { message.stopSequences = TextArray.decode(reader, reader.uint32()); continue; + case 8: + if (tag !== 66) { + break; + } + + message.images = TextArray.decode(reader, reader.uint32()); + continue; + case 9: + if (tag !== 74) { + break; + } + + message.imageProperties = TextArray.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -985,6 +1055,8 @@ export const GenerativeAnthropic = { topK: isSet(object.topK) ? globalThis.Number(object.topK) : undefined, topP: isSet(object.topP) ? globalThis.Number(object.topP) : undefined, stopSequences: isSet(object.stopSequences) ? TextArray.fromJSON(object.stopSequences) : undefined, + images: isSet(object.images) ? TextArray.fromJSON(object.images) : undefined, + imageProperties: isSet(object.imageProperties) ? TextArray.fromJSON(object.imageProperties) : undefined, }; }, @@ -1011,6 +1083,12 @@ export const GenerativeAnthropic = { if (message.stopSequences !== undefined) { obj.stopSequences = TextArray.toJSON(message.stopSequences); } + if (message.images !== undefined) { + obj.images = TextArray.toJSON(message.images); + } + if (message.imageProperties !== undefined) { + obj.imageProperties = TextArray.toJSON(message.imageProperties); + } return obj; }, @@ -1028,6 +1106,12 @@ export const GenerativeAnthropic = { message.stopSequences = (object.stopSequences !== undefined && object.stopSequences !== null) ? TextArray.fromPartial(object.stopSequences) : undefined; + message.images = (object.images !== undefined && object.images !== null) + ? TextArray.fromPartial(object.images) + : undefined; + message.imageProperties = (object.imageProperties !== undefined && object.imageProperties !== null) + ? TextArray.fromPartial(object.imageProperties) + : undefined; return message; }, }; @@ -1130,6 +1214,8 @@ function createBaseGenerativeAWS(): GenerativeAWS { endpoint: undefined, targetModel: undefined, targetVariant: undefined, + images: undefined, + imageProperties: undefined, }; } @@ -1156,6 +1242,12 @@ export const GenerativeAWS = { if (message.targetVariant !== undefined) { writer.uint32(106).string(message.targetVariant); } + if (message.images !== undefined) { + TextArray.encode(message.images, writer.uint32(114).fork()).ldelim(); + } + if (message.imageProperties !== undefined) { + TextArray.encode(message.imageProperties, writer.uint32(122).fork()).ldelim(); + } return writer; }, @@ -1215,6 +1307,20 @@ export const GenerativeAWS = { message.targetVariant = reader.string(); continue; + case 14: + if (tag !== 114) { + break; + } + + message.images = TextArray.decode(reader, reader.uint32()); + continue; + case 15: + if (tag !== 122) { + break; + } + + message.imageProperties = TextArray.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1233,6 +1339,8 @@ export const GenerativeAWS = { endpoint: isSet(object.endpoint) ? globalThis.String(object.endpoint) : undefined, targetModel: isSet(object.targetModel) ? globalThis.String(object.targetModel) : undefined, targetVariant: isSet(object.targetVariant) ? globalThis.String(object.targetVariant) : undefined, + images: isSet(object.images) ? TextArray.fromJSON(object.images) : undefined, + imageProperties: isSet(object.imageProperties) ? TextArray.fromJSON(object.imageProperties) : undefined, }; }, @@ -1259,6 +1367,12 @@ export const GenerativeAWS = { if (message.targetVariant !== undefined) { obj.targetVariant = message.targetVariant; } + if (message.images !== undefined) { + obj.images = TextArray.toJSON(message.images); + } + if (message.imageProperties !== undefined) { + obj.imageProperties = TextArray.toJSON(message.imageProperties); + } return obj; }, @@ -1274,6 +1388,12 @@ export const GenerativeAWS = { message.endpoint = object.endpoint ?? undefined; message.targetModel = object.targetModel ?? undefined; message.targetVariant = object.targetVariant ?? undefined; + message.images = (object.images !== undefined && object.images !== null) + ? TextArray.fromPartial(object.images) + : undefined; + message.imageProperties = (object.imageProperties !== undefined && object.imageProperties !== null) + ? TextArray.fromPartial(object.imageProperties) + : undefined; return message; }, }; @@ -1632,7 +1752,13 @@ export const GenerativeMistral = { }; function createBaseGenerativeOllama(): GenerativeOllama { - return { apiEndpoint: undefined, model: undefined, temperature: undefined }; + return { + apiEndpoint: undefined, + model: undefined, + temperature: undefined, + images: undefined, + imageProperties: undefined, + }; } export const GenerativeOllama = { @@ -1646,6 +1772,12 @@ export const GenerativeOllama = { if (message.temperature !== undefined) { writer.uint32(25).double(message.temperature); } + if (message.images !== undefined) { + TextArray.encode(message.images, writer.uint32(34).fork()).ldelim(); + } + if (message.imageProperties !== undefined) { + TextArray.encode(message.imageProperties, writer.uint32(42).fork()).ldelim(); + } return writer; }, @@ -1677,6 +1809,20 @@ export const GenerativeOllama = { message.temperature = reader.double(); continue; + case 4: + if (tag !== 34) { + break; + } + + message.images = TextArray.decode(reader, reader.uint32()); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.imageProperties = TextArray.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1691,6 +1837,8 @@ export const GenerativeOllama = { apiEndpoint: isSet(object.apiEndpoint) ? globalThis.String(object.apiEndpoint) : undefined, model: isSet(object.model) ? globalThis.String(object.model) : undefined, temperature: isSet(object.temperature) ? globalThis.Number(object.temperature) : undefined, + images: isSet(object.images) ? TextArray.fromJSON(object.images) : undefined, + imageProperties: isSet(object.imageProperties) ? TextArray.fromJSON(object.imageProperties) : undefined, }; }, @@ -1705,6 +1853,12 @@ export const GenerativeOllama = { if (message.temperature !== undefined) { obj.temperature = message.temperature; } + if (message.images !== undefined) { + obj.images = TextArray.toJSON(message.images); + } + if (message.imageProperties !== undefined) { + obj.imageProperties = TextArray.toJSON(message.imageProperties); + } return obj; }, @@ -1716,6 +1870,12 @@ export const GenerativeOllama = { message.apiEndpoint = object.apiEndpoint ?? undefined; message.model = object.model ?? undefined; message.temperature = object.temperature ?? undefined; + message.images = (object.images !== undefined && object.images !== null) + ? TextArray.fromPartial(object.images) + : undefined; + message.imageProperties = (object.imageProperties !== undefined && object.imageProperties !== null) + ? TextArray.fromPartial(object.imageProperties) + : undefined; return message; }, }; @@ -1735,6 +1895,8 @@ function createBaseGenerativeOpenAI(): GenerativeOpenAI { resourceName: undefined, deploymentId: undefined, isAzure: undefined, + images: undefined, + imageProperties: undefined, }; } @@ -1779,6 +1941,12 @@ export const GenerativeOpenAI = { if (message.isAzure !== undefined) { writer.uint32(104).bool(message.isAzure); } + if (message.images !== undefined) { + TextArray.encode(message.images, writer.uint32(114).fork()).ldelim(); + } + if (message.imageProperties !== undefined) { + TextArray.encode(message.imageProperties, writer.uint32(122).fork()).ldelim(); + } return writer; }, @@ -1880,6 +2048,20 @@ export const GenerativeOpenAI = { message.isAzure = reader.bool(); continue; + case 14: + if (tag !== 114) { + break; + } + + message.images = TextArray.decode(reader, reader.uint32()); + continue; + case 15: + if (tag !== 122) { + break; + } + + message.imageProperties = TextArray.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1904,6 +2086,8 @@ export const GenerativeOpenAI = { resourceName: isSet(object.resourceName) ? globalThis.String(object.resourceName) : undefined, deploymentId: isSet(object.deploymentId) ? globalThis.String(object.deploymentId) : undefined, isAzure: isSet(object.isAzure) ? globalThis.Boolean(object.isAzure) : undefined, + images: isSet(object.images) ? TextArray.fromJSON(object.images) : undefined, + imageProperties: isSet(object.imageProperties) ? TextArray.fromJSON(object.imageProperties) : undefined, }; }, @@ -1948,6 +2132,12 @@ export const GenerativeOpenAI = { if (message.isAzure !== undefined) { obj.isAzure = message.isAzure; } + if (message.images !== undefined) { + obj.images = TextArray.toJSON(message.images); + } + if (message.imageProperties !== undefined) { + obj.imageProperties = TextArray.toJSON(message.imageProperties); + } return obj; }, @@ -1969,6 +2159,12 @@ export const GenerativeOpenAI = { message.resourceName = object.resourceName ?? undefined; message.deploymentId = object.deploymentId ?? undefined; message.isAzure = object.isAzure ?? undefined; + message.images = (object.images !== undefined && object.images !== null) + ? TextArray.fromPartial(object.images) + : undefined; + message.imageProperties = (object.imageProperties !== undefined && object.imageProperties !== null) + ? TextArray.fromPartial(object.imageProperties) + : undefined; return message; }, }; @@ -1987,6 +2183,8 @@ function createBaseGenerativeGoogle(): GenerativeGoogle { projectId: undefined, endpointId: undefined, region: undefined, + images: undefined, + imageProperties: undefined, }; } @@ -2028,6 +2226,12 @@ export const GenerativeGoogle = { if (message.region !== undefined) { writer.uint32(98).string(message.region); } + if (message.images !== undefined) { + TextArray.encode(message.images, writer.uint32(106).fork()).ldelim(); + } + if (message.imageProperties !== undefined) { + TextArray.encode(message.imageProperties, writer.uint32(114).fork()).ldelim(); + } return writer; }, @@ -2122,6 +2326,20 @@ export const GenerativeGoogle = { message.region = reader.string(); continue; + case 13: + if (tag !== 106) { + break; + } + + message.images = TextArray.decode(reader, reader.uint32()); + continue; + case 14: + if (tag !== 114) { + break; + } + + message.imageProperties = TextArray.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -2145,6 +2363,8 @@ export const GenerativeGoogle = { projectId: isSet(object.projectId) ? globalThis.String(object.projectId) : undefined, endpointId: isSet(object.endpointId) ? globalThis.String(object.endpointId) : undefined, region: isSet(object.region) ? globalThis.String(object.region) : undefined, + images: isSet(object.images) ? TextArray.fromJSON(object.images) : undefined, + imageProperties: isSet(object.imageProperties) ? TextArray.fromJSON(object.imageProperties) : undefined, }; }, @@ -2186,6 +2406,12 @@ export const GenerativeGoogle = { if (message.region !== undefined) { obj.region = message.region; } + if (message.images !== undefined) { + obj.images = TextArray.toJSON(message.images); + } + if (message.imageProperties !== undefined) { + obj.imageProperties = TextArray.toJSON(message.imageProperties); + } return obj; }, @@ -2208,6 +2434,12 @@ export const GenerativeGoogle = { message.projectId = object.projectId ?? undefined; message.endpointId = object.endpointId ?? undefined; message.region = object.region ?? undefined; + message.images = (object.images !== undefined && object.images !== null) + ? TextArray.fromPartial(object.images) + : undefined; + message.imageProperties = (object.imageProperties !== undefined && object.imageProperties !== null) + ? TextArray.fromPartial(object.imageProperties) + : undefined; return message; }, }; @@ -2574,6 +2806,125 @@ export const GenerativeFriendliAI = { }, }; +function createBaseGenerativeNvidia(): GenerativeNvidia { + return { baseUrl: undefined, model: undefined, temperature: undefined, topP: undefined, maxTokens: undefined }; +} + +export const GenerativeNvidia = { + encode(message: GenerativeNvidia, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.baseUrl !== undefined) { + writer.uint32(10).string(message.baseUrl); + } + if (message.model !== undefined) { + writer.uint32(18).string(message.model); + } + if (message.temperature !== undefined) { + writer.uint32(25).double(message.temperature); + } + if (message.topP !== undefined) { + writer.uint32(33).double(message.topP); + } + if (message.maxTokens !== undefined) { + writer.uint32(40).int64(message.maxTokens); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): GenerativeNvidia { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGenerativeNvidia(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.baseUrl = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.model = reader.string(); + continue; + case 3: + if (tag !== 25) { + break; + } + + message.temperature = reader.double(); + continue; + case 4: + if (tag !== 33) { + break; + } + + message.topP = reader.double(); + continue; + case 5: + if (tag !== 40) { + break; + } + + message.maxTokens = longToNumber(reader.int64() as Long); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): GenerativeNvidia { + return { + baseUrl: isSet(object.baseUrl) ? globalThis.String(object.baseUrl) : undefined, + model: isSet(object.model) ? globalThis.String(object.model) : undefined, + temperature: isSet(object.temperature) ? globalThis.Number(object.temperature) : undefined, + topP: isSet(object.topP) ? globalThis.Number(object.topP) : undefined, + maxTokens: isSet(object.maxTokens) ? globalThis.Number(object.maxTokens) : undefined, + }; + }, + + toJSON(message: GenerativeNvidia): unknown { + const obj: any = {}; + if (message.baseUrl !== undefined) { + obj.baseUrl = message.baseUrl; + } + if (message.model !== undefined) { + obj.model = message.model; + } + if (message.temperature !== undefined) { + obj.temperature = message.temperature; + } + if (message.topP !== undefined) { + obj.topP = message.topP; + } + if (message.maxTokens !== undefined) { + obj.maxTokens = Math.round(message.maxTokens); + } + return obj; + }, + + create(base?: DeepPartial): GenerativeNvidia { + return GenerativeNvidia.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GenerativeNvidia { + const message = createBaseGenerativeNvidia(); + message.baseUrl = object.baseUrl ?? undefined; + message.model = object.model ?? undefined; + message.temperature = object.temperature ?? undefined; + message.topP = object.topP ?? undefined; + message.maxTokens = object.maxTokens ?? undefined; + return message; + }, +}; + function createBaseGenerativeAnthropicMetadata(): GenerativeAnthropicMetadata { return { usage: undefined }; } @@ -4246,6 +4597,154 @@ export const GenerativeFriendliAIMetadata_Usage = { }, }; +function createBaseGenerativeNvidiaMetadata(): GenerativeNvidiaMetadata { + return { usage: undefined }; +} + +export const GenerativeNvidiaMetadata = { + encode(message: GenerativeNvidiaMetadata, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.usage !== undefined) { + GenerativeNvidiaMetadata_Usage.encode(message.usage, writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): GenerativeNvidiaMetadata { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGenerativeNvidiaMetadata(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.usage = GenerativeNvidiaMetadata_Usage.decode(reader, reader.uint32()); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): GenerativeNvidiaMetadata { + return { usage: isSet(object.usage) ? GenerativeNvidiaMetadata_Usage.fromJSON(object.usage) : undefined }; + }, + + toJSON(message: GenerativeNvidiaMetadata): unknown { + const obj: any = {}; + if (message.usage !== undefined) { + obj.usage = GenerativeNvidiaMetadata_Usage.toJSON(message.usage); + } + return obj; + }, + + create(base?: DeepPartial): GenerativeNvidiaMetadata { + return GenerativeNvidiaMetadata.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GenerativeNvidiaMetadata { + const message = createBaseGenerativeNvidiaMetadata(); + message.usage = (object.usage !== undefined && object.usage !== null) + ? GenerativeNvidiaMetadata_Usage.fromPartial(object.usage) + : undefined; + return message; + }, +}; + +function createBaseGenerativeNvidiaMetadata_Usage(): GenerativeNvidiaMetadata_Usage { + return { promptTokens: undefined, completionTokens: undefined, totalTokens: undefined }; +} + +export const GenerativeNvidiaMetadata_Usage = { + encode(message: GenerativeNvidiaMetadata_Usage, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.promptTokens !== undefined) { + writer.uint32(8).int64(message.promptTokens); + } + if (message.completionTokens !== undefined) { + writer.uint32(16).int64(message.completionTokens); + } + if (message.totalTokens !== undefined) { + writer.uint32(24).int64(message.totalTokens); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): GenerativeNvidiaMetadata_Usage { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGenerativeNvidiaMetadata_Usage(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 8) { + break; + } + + message.promptTokens = longToNumber(reader.int64() as Long); + continue; + case 2: + if (tag !== 16) { + break; + } + + message.completionTokens = longToNumber(reader.int64() as Long); + continue; + case 3: + if (tag !== 24) { + break; + } + + message.totalTokens = longToNumber(reader.int64() as Long); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): GenerativeNvidiaMetadata_Usage { + return { + promptTokens: isSet(object.promptTokens) ? globalThis.Number(object.promptTokens) : undefined, + completionTokens: isSet(object.completionTokens) ? globalThis.Number(object.completionTokens) : undefined, + totalTokens: isSet(object.totalTokens) ? globalThis.Number(object.totalTokens) : undefined, + }; + }, + + toJSON(message: GenerativeNvidiaMetadata_Usage): unknown { + const obj: any = {}; + if (message.promptTokens !== undefined) { + obj.promptTokens = Math.round(message.promptTokens); + } + if (message.completionTokens !== undefined) { + obj.completionTokens = Math.round(message.completionTokens); + } + if (message.totalTokens !== undefined) { + obj.totalTokens = Math.round(message.totalTokens); + } + return obj; + }, + + create(base?: DeepPartial): GenerativeNvidiaMetadata_Usage { + return GenerativeNvidiaMetadata_Usage.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): GenerativeNvidiaMetadata_Usage { + const message = createBaseGenerativeNvidiaMetadata_Usage(); + message.promptTokens = object.promptTokens ?? undefined; + message.completionTokens = object.completionTokens ?? undefined; + message.totalTokens = object.totalTokens ?? undefined; + return message; + }, +}; + function createBaseGenerativeMetadata(): GenerativeMetadata { return { anthropic: undefined, @@ -4259,6 +4758,7 @@ function createBaseGenerativeMetadata(): GenerativeMetadata { google: undefined, databricks: undefined, friendliai: undefined, + nvidia: undefined, }; } @@ -4297,6 +4797,9 @@ export const GenerativeMetadata = { if (message.friendliai !== undefined) { GenerativeFriendliAIMetadata.encode(message.friendliai, writer.uint32(90).fork()).ldelim(); } + if (message.nvidia !== undefined) { + GenerativeNvidiaMetadata.encode(message.nvidia, writer.uint32(98).fork()).ldelim(); + } return writer; }, @@ -4384,6 +4887,13 @@ export const GenerativeMetadata = { message.friendliai = GenerativeFriendliAIMetadata.decode(reader, reader.uint32()); continue; + case 12: + if (tag !== 98) { + break; + } + + message.nvidia = GenerativeNvidiaMetadata.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -4406,6 +4916,7 @@ export const GenerativeMetadata = { google: isSet(object.google) ? GenerativeGoogleMetadata.fromJSON(object.google) : undefined, databricks: isSet(object.databricks) ? GenerativeDatabricksMetadata.fromJSON(object.databricks) : undefined, friendliai: isSet(object.friendliai) ? GenerativeFriendliAIMetadata.fromJSON(object.friendliai) : undefined, + nvidia: isSet(object.nvidia) ? GenerativeNvidiaMetadata.fromJSON(object.nvidia) : undefined, }; }, @@ -4444,6 +4955,9 @@ export const GenerativeMetadata = { if (message.friendliai !== undefined) { obj.friendliai = GenerativeFriendliAIMetadata.toJSON(message.friendliai); } + if (message.nvidia !== undefined) { + obj.nvidia = GenerativeNvidiaMetadata.toJSON(message.nvidia); + } return obj; }, @@ -4485,6 +4999,9 @@ export const GenerativeMetadata = { message.friendliai = (object.friendliai !== undefined && object.friendliai !== null) ? GenerativeFriendliAIMetadata.fromPartial(object.friendliai) : undefined; + message.nvidia = (object.nvidia !== undefined && object.nvidia !== null) + ? GenerativeNvidiaMetadata.fromPartial(object.nvidia) + : undefined; return message; }, }; diff --git a/src/utils/dbVersion.ts b/src/utils/dbVersion.ts index 279537e2..707b5f26 100644 --- a/src/utils/dbVersion.ts +++ b/src/utils/dbVersion.ts @@ -219,6 +219,17 @@ export class DbVersionSupport { }; }); }; + + supportsSingleGrouped = () => + this.dbVersionProvider.getVersion().then((version) => ({ + version, + supports: + (version.isAtLeast(1, 27, 14) && version.isLowerThan(1, 28, 0)) || + (version.isAtLeast(1, 28, 8) && version.isLowerThan(1, 29, 0)) || + (version.isAtLeast(1, 29, 0) && version.isLowerThan(1, 30, 0)) || + version.isAtLeast(1, 30, 0), + message: this.errorMessage('Single/Grouped fields in gRPC', version.show(), '1.30.0'), + })); } const EMPTY_VERSION = ''; From a50f56f32e4fc7a9b41c5740a5ec437857cf8a63 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Fri, 7 Mar 2025 11:53:46 +0000 Subject: [PATCH 02/49] Fix unit test --- src/collections/serialize/unit.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/collections/serialize/unit.test.ts b/src/collections/serialize/unit.test.ts index 721d1e46..d6d5d627 100644 --- a/src/collections/serialize/unit.test.ts +++ b/src/collections/serialize/unit.test.ts @@ -442,11 +442,14 @@ describe('Unit testing of Serialize', () => { }); it('should parse args for generative', () => { - const args = Serialize.generative({ - singlePrompt: 'test', - groupedProperties: ['name'], - groupedTask: 'testing', - }); + const args = Serialize.generative( + { supportsSingleGrouped: false }, + { + singlePrompt: 'test', + groupedProperties: ['name'], + groupedTask: 'testing', + } + ); expect(args).toEqual({ singleResponsePrompt: 'test', groupedProperties: ['name'], From 5eb09da5b65fdb06f5be96eae4f3d1e9a784502e Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Fri, 7 Mar 2025 13:55:38 +0000 Subject: [PATCH 03/49] Again fix unit test --- src/collections/serialize/unit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/collections/serialize/unit.test.ts b/src/collections/serialize/unit.test.ts index d6d5d627..6d9f9612 100644 --- a/src/collections/serialize/unit.test.ts +++ b/src/collections/serialize/unit.test.ts @@ -441,8 +441,8 @@ describe('Unit testing of Serialize', () => { }); }); - it('should parse args for generative', () => { - const args = Serialize.generative( + it('should parse args for generative', async () => { + const args = await Serialize.generative( { supportsSingleGrouped: false }, { singlePrompt: 'test', From c16cbd3199304492b20cc4b9ce2a37a0091c0c6c Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Fri, 7 Mar 2025 14:40:38 +0000 Subject: [PATCH 04/49] Add concurrency limit to CI on a per branch basis to cancel old runs --- .github/workflows/main.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3595cb1f..cecd2521 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,6 +15,10 @@ env: WEAVIATE_129: 1.29.0 WEAVIATE_130: 1.30.0-dev-680e323 +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: checks: runs-on: ubuntu-latest From befa2affbab51f7505634ce3ba555fa8875a7641 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 10 Mar 2025 09:15:59 +0000 Subject: [PATCH 05/49] Add test of string-only usage with runtime generative --- src/collections/generate/integration.test.ts | 33 +++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/collections/generate/integration.test.ts b/src/collections/generate/integration.test.ts index 5e478093..b93a2286 100644 --- a/src/collections/generate/integration.test.ts +++ b/src/collections/generate/integration.test.ts @@ -460,7 +460,7 @@ maybe('Testing of the collection.generate methods with runtime generative config }); }); - it('should generate using a runtime config without search', async () => { + it('should generate using a runtime config without search and with extras', async () => { const query = () => collection.generate.fetchObjects({ singlePrompt: { @@ -498,4 +498,35 @@ maybe('Testing of the collection.generate methods with runtime generative config expect(obj.generative?.debug).toBeDefined(); }); }); + + it('should generate using a runtime config without search nor extras', async () => { + const query = () => + collection.generate.fetchObjects({ + singlePrompt: 'Write a haiku about ducks for {testProp}', + groupedTask: 'What is the value of testProp here?', + config: { + name: 'generative-openai', + config: { + model: 'gpt-4o-mini', + }, + }, + }); + + if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 30, 0))) { + await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); + return; + } + + const res = await query(); + expect(res.objects.length).toEqual(1); + expect(res.generated).toBeDefined(); + expect(res.generative?.text).toBeDefined(); + expect(res.generative?.metadata).toBeUndefined(); + res.objects.forEach((obj) => { + expect(obj.generated).toBeDefined(); + expect(obj.generative?.text).toBeDefined(); + expect(obj.generative?.metadata).toBeUndefined(); + expect(obj.generative?.debug).toBeUndefined(); + }); + }); }); From c688c838f87700016a77a08913bc8e6008be718f Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 10 Mar 2025 11:47:28 +0000 Subject: [PATCH 06/49] Add factory to produce user friendly gen runtime config objects --- src/collections/config/types/generative.ts | 7 + src/collections/configure/generative.ts | 18 ++ src/collections/configure/types/generative.ts | 6 + src/collections/generate/config.ts | 282 ++++++++++++++++++ src/collections/generate/index.ts | 1 + src/collections/generate/integration.test.ts | 15 +- src/collections/generate/unit.test.ts | 280 +++++++++++++++++ src/collections/types/generate.ts | 181 +++++++++-- 8 files changed, 757 insertions(+), 33 deletions(-) create mode 100644 src/collections/generate/config.ts create mode 100644 src/collections/generate/unit.test.ts diff --git a/src/collections/config/types/generative.ts b/src/collections/config/types/generative.ts index 667bc347..7ff426b5 100644 --- a/src/collections/config/types/generative.ts +++ b/src/collections/config/types/generative.ts @@ -58,6 +58,13 @@ export type GenerativeMistralConfig = { temperature?: number; }; +export type GenerativeNvidiaConfig = { + baseURL?: string; + maxTokens?: number; + model?: string; + temperature?: number; +}; + export type GenerativeOllamaConfig = { apiEndpoint?: string; model?: string; diff --git a/src/collections/configure/generative.ts b/src/collections/configure/generative.ts index 730f2bcb..d4ee3154 100644 --- a/src/collections/configure/generative.ts +++ b/src/collections/configure/generative.ts @@ -8,6 +8,7 @@ import { GenerativeFriendliAIConfig, GenerativeGoogleConfig, GenerativeMistralConfig, + GenerativeNvidiaConfig, GenerativeOllamaConfig, GenerativeOpenAIConfig, GenerativePaLMConfig, @@ -22,6 +23,7 @@ import { GenerativeDatabricksConfigCreate, GenerativeFriendliAIConfigCreate, GenerativeMistralConfigCreate, + GenerativeNvidiaConfigCreate, GenerativeOllamaConfigCreate, GenerativeOpenAIConfigCreate, GenerativePaLMConfigCreate, @@ -169,6 +171,22 @@ export default { config, }; }, + /** + * Create a `ModuleConfig<'generative-nvidia', GenerativeNvidiaConfig | undefined>` object for use when performing AI generation using the `generative-mistral` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/nvidia/generative) for detailed usage. + * + * @param {GenerativeNvidiaConfigCreate} [config] The configuration for the `generative-nvidia` module. + * @returns {ModuleConfig<'generative-nvidia', GenerativeNvidiaConfig | undefined>} The configuration object. + */ + nvidia( + config?: GenerativeNvidiaConfigCreate + ): ModuleConfig<'generative-nvidia', GenerativeNvidiaConfig | undefined> { + return { + name: 'generative-nvidia', + config, + }; + }, /** * Create a `ModuleConfig<'generative-ollama', GenerativeOllamaConfig | undefined>` object for use when performing AI generation using the `generative-ollama` module. * diff --git a/src/collections/configure/types/generative.ts b/src/collections/configure/types/generative.ts index 2b1a18cf..ccf22ec6 100644 --- a/src/collections/configure/types/generative.ts +++ b/src/collections/configure/types/generative.ts @@ -5,6 +5,7 @@ import { GenerativeDatabricksConfig, GenerativeFriendliAIConfig, GenerativeMistralConfig, + GenerativeNvidiaConfig, GenerativeOllamaConfig, GenerativePaLMConfig, } from '../../index.js'; @@ -44,6 +45,8 @@ export type GenerativeFriendliAIConfigCreate = GenerativeFriendliAIConfig; export type GenerativeMistralConfigCreate = GenerativeMistralConfig; +export type GenerativeNvidiaConfigCreate = GenerativeNvidiaConfig; + export type GenerativeOllamaConfigCreate = GenerativeOllamaConfig; export type GenerativeOpenAIConfigCreate = GenerativeOpenAIConfigBaseCreate & { @@ -61,6 +64,7 @@ export type GenerativeConfigCreate = | GenerativeDatabricksConfigCreate | GenerativeFriendliAIConfigCreate | GenerativeMistralConfigCreate + | GenerativeNvidiaConfigCreate | GenerativeOllamaConfigCreate | GenerativeOpenAIConfigCreate | GenerativePaLMConfigCreate @@ -81,6 +85,8 @@ export type GenerativeConfigCreateType = G extends 'generative-anthropic' ? GenerativeFriendliAIConfigCreate : G extends 'generative-mistral' ? GenerativeMistralConfigCreate + : G extends 'generative-nvidia' + ? GenerativeNvidiaConfigCreate : G extends 'generative-ollama' ? GenerativeOllamaConfigCreate : G extends 'generative-openai' diff --git a/src/collections/generate/config.ts b/src/collections/generate/config.ts new file mode 100644 index 00000000..a633bacc --- /dev/null +++ b/src/collections/generate/config.ts @@ -0,0 +1,282 @@ +import { TextArray } from '../../proto/v1/base.js'; +import { ModuleConfig } from '../config/types/index.js'; +import { + GenerativeAWSConfigRuntime, + GenerativeAnthropicConfigRuntime, + GenerativeAnyscaleConfigRuntime, + GenerativeCohereConfigRuntime, + GenerativeConfigRuntimeType, + GenerativeDatabricksConfigRuntime, + GenerativeFriendliAIConfigRuntime, + GenerativeGoogleConfigRuntime, + GenerativeMistralConfigRuntime, + GenerativeNvidiaConfigRuntime, + GenerativeOllamaConfigRuntime, + GenerativeOpenAIConfigRuntime, +} from '../index.js'; + +export const generativeConfigRuntime = { + /** + * Create a `ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-anthropic` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/anthropic/generative) for detailed usage. + * + * @param {GenerativeAnthropicConfigCreateRuntime} [config] The configuration for the `generative-anthropic` module. + * @returns {ModuleConfig<'generative-anthropic', GenerativeAnthropicConfigCreateRuntime | undefined>} The configuration object. + */ + anthropic( + config?: GenerativeAnthropicConfigRuntime + ): ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined> { + const { baseURL, stopSequences, ...rest } = config || {}; + return { + name: 'generative-anthropic', + config: config + ? { + ...rest, + baseUrl: baseURL, + stopSequences: TextArray.fromPartial({ values: stopSequences }), + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-anyscale` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/anyscale/generative) for detailed usage. + * + * @param {GenerativeAnyscaleConfigRuntime} [config] The configuration for the `generative-aws` module. + * @returns {ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined>} The configuration object. + */ + anyscale( + config?: GenerativeAnyscaleConfigRuntime + ): ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined> { + const { baseURL, ...rest } = config || {}; + return { + name: 'generative-anyscale', + config: config + ? { + ...rest, + baseUrl: baseURL, + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-aws` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/aws/generative) for detailed usage. + * + * @param {GenerativeAWSConfigRuntime} [config] The configuration for the `generative-aws` module. + * @returns {ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined>} The configuration object. + */ + aws( + config?: GenerativeAWSConfigRuntime + ): ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined> { + return { + name: 'generative-aws', + config, + }; + }, + /** + * Create a `ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>>` object for use when performing runtime-specific AI generation using the `generative-openai` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/openai/generative) for detailed usage. + * + * @param {GenerativeAzureOpenAIConfigRuntime} [config] The configuration for the `generative-openai` module. + * @returns {ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>>} The configuration object. + */ + azureOpenAI: ( + config?: GenerativeOpenAIConfigRuntime + ): ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> => { + const { baseURL, model, stop, ...rest } = config || {}; + return { + name: 'generative-azure-openai', + config: config + ? { + ...rest, + baseUrl: baseURL, + model: model ?? '', + isAzure: true, + stop: TextArray.fromPartial({ values: stop }), + } + : { model: '', isAzure: true }, + }; + }, + /** + * Create a `ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-cohere` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/cohere/generative) for detailed usage. + * + * @param {GenerativeCohereConfigRuntime} [config] The configuration for the `generative-cohere` module. + * @returns {ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined>} The configuration object. + */ + cohere: ( + config?: GenerativeCohereConfigRuntime + ): ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined> => { + const { baseURL, stopSequences, ...rest } = config || {}; + return { + name: 'generative-cohere', + config: config + ? { + ...rest, + baseUrl: baseURL, + stopSequences: TextArray.fromPartial({ values: stopSequences }), + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-databricks'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-databricks` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/databricks/generative) for detailed usage. + * + * @param {GenerativeDatabricksConfigRuntime} [config] The configuration for the `generative-databricks` module. + * @returns {ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-databricks'> | undefined>} The configuration object. + */ + databricks: ( + config?: GenerativeDatabricksConfigRuntime + ): ModuleConfig< + 'generative-databricks', + GenerativeConfigRuntimeType<'generative-databricks'> | undefined + > => { + const { stop, ...rest } = config || {}; + return { + name: 'generative-databricks', + config: config + ? { + ...rest, + stop: TextArray.fromPartial({ values: stop }), + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-friendliai', GenerativeConfigRuntimeType<'generative-friendliai'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-friendliai` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/friendliai/generative) for detailed usage. + * + * @param {GenerativeFriendliAIConfigRuntime} [config] The configuration for the `generative-friendliai` module. + * @returns {ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-friendliai'> | undefined>} The configuration object. + */ + friendliai( + config?: GenerativeFriendliAIConfigRuntime + ): ModuleConfig<'generative-friendliai', GenerativeConfigRuntimeType<'generative-friendliai'> | undefined> { + const { baseURL, ...rest } = config || {}; + return { + name: 'generative-friendliai', + config: config + ? { + ...rest, + baseUrl: baseURL, + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-mistral` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/mistral/generative) for detailed usage. + * + * @param {GenerativeMistralConfigRuntime} [config] The configuration for the `generative-mistral` module. + * @returns {ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined>} The configuration object. + */ + mistral( + config?: GenerativeMistralConfigRuntime + ): ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined> { + const { baseURL, ...rest } = config || {}; + return { + name: 'generative-mistral', + config: config + ? { + baseUrl: baseURL, + ...rest, + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-mistral` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/nvidia/generative) for detailed usage. + * + * @param {GenerativeNvidiaConfigCreate} [config] The configuration for the `generative-nvidia` module. + * @returns {ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined>} The configuration object. + */ + nvidia( + config?: GenerativeNvidiaConfigRuntime + ): ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined> { + const { baseURL, ...rest } = config || {}; + return { + name: 'generative-nvidia', + config: config + ? { + ...rest, + baseUrl: baseURL, + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-ollama` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/ollama/generative) for detailed usage. + * + * @param {GenerativeOllamaConfigRuntime} [config] The configuration for the `generative-openai` module. + * @returns {ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined>} The configuration object. + */ + ollama( + config?: GenerativeOllamaConfigRuntime + ): ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined> { + return { + name: 'generative-ollama', + config, + }; + }, + /** + * Create a `ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>>` object for use when performing runtime-specific AI generation using the `generative-openai` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/openai/generative) for detailed usage. + * + * @param {GenerativeOpenAIConfigRuntime} [config] The configuration for the `generative-openai` module. + * @returns {ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>>} The configuration object. + */ + openAI: ( + config?: GenerativeOpenAIConfigRuntime + ): ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> => { + const { baseURL, model, stop, ...rest } = config || {}; + return { + name: 'generative-openai', + config: config + ? { + ...rest, + baseUrl: baseURL, + model: model ?? '', + isAzure: false, + stop: TextArray.fromPartial({ values: stop }), + } + : { model: '', isAzure: false }, + }; + }, + /** + * Create a `ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-openai'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-google` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/google/generative) for detailed usage. + * + * @param {GenerativeGoogleConfigRuntime} [config] The configuration for the `generative-palm` module. + * @returns {ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-google'> | undefined>} The configuration object. + */ + google: ( + config?: GenerativeGoogleConfigRuntime + ): ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-google'> | undefined> => { + const { stopSequences, ...rest } = config || {}; + return { + name: 'generative-google', + config: config + ? { + ...rest, + stopSequences: TextArray.fromPartial({ values: stopSequences }), + } + : undefined, + }; + }, +}; diff --git a/src/collections/generate/index.ts b/src/collections/generate/index.ts index 60de4f94..3ebcff77 100644 --- a/src/collections/generate/index.ts +++ b/src/collections/generate/index.ts @@ -364,4 +364,5 @@ class GenerateManager implements Generate { export default GenerateManager.use; +export { generativeConfigRuntime } from './config.js'; export { Generate } from './types.js'; diff --git a/src/collections/generate/integration.test.ts b/src/collections/generate/integration.test.ts index b93a2286..3b6708fd 100644 --- a/src/collections/generate/integration.test.ts +++ b/src/collections/generate/integration.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ import { WeaviateUnsupportedFeatureError } from '../../errors.js'; -import weaviate, { WeaviateClient } from '../../index.js'; +import weaviate, { WeaviateClient, generativeConfigRuntime } from '../../index.js'; import { Collection } from '../collection/index.js'; import { GenerateOptions, GroupByOptions } from '../types/index.js'; @@ -460,7 +460,7 @@ maybe('Testing of the collection.generate methods with runtime generative config }); }); - it('should generate using a runtime config without search and with extras', async () => { + it.only('should generate using a runtime config without search and with extras', async () => { const query = () => collection.generate.fetchObjects({ singlePrompt: { @@ -473,12 +473,10 @@ maybe('Testing of the collection.generate methods with runtime generative config nonBlobProperties: ['testProp'], metadata: true, }, - config: { - name: 'generative-openai', - config: { - model: 'gpt-4o-mini', - }, - }, + config: generativeConfigRuntime.openAI({ + model: 'gpt-4o-mini', + stop: ['\n'], + }), }); if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 30, 0))) { @@ -508,6 +506,7 @@ maybe('Testing of the collection.generate methods with runtime generative config name: 'generative-openai', config: { model: 'gpt-4o-mini', + stop: { values: ['\n'] }, }, }, }); diff --git a/src/collections/generate/unit.test.ts b/src/collections/generate/unit.test.ts new file mode 100644 index 00000000..e2e296f0 --- /dev/null +++ b/src/collections/generate/unit.test.ts @@ -0,0 +1,280 @@ +import { GenerativeConfigRuntimeType, ModuleConfig } from '../types'; +import { generativeConfigRuntime } from './config'; + +// only tests fields that must be mapped from some public name to a gRPC name, e.g. baseURL -> baseUrl and stop: string[] -> stop: TextArray +describe('Unit testing of the generativeConfigRuntime factory methods', () => { + describe('anthropic', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.anthropic(); + expect(config).toEqual< + ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined> + >({ + name: 'generative-anthropic', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.anthropic({ + baseURL: 'http://localhost:8080', + stopSequences: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined> + >({ + name: 'generative-anthropic', + config: { + baseUrl: 'http://localhost:8080', + stopSequences: { values: ['a', 'b', 'c'] }, + }, + }); + }); + }); + + describe('anyscale', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.anyscale(); + expect(config).toEqual< + ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined> + >({ + name: 'generative-anyscale', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.anyscale({ + baseURL: 'http://localhost:8080', + }); + expect(config).toEqual< + ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined> + >({ + name: 'generative-anyscale', + config: { + baseUrl: 'http://localhost:8080', + }, + }); + }); + }); + + describe('aws', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.aws(); + expect(config).toEqual< + ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined> + >({ + name: 'generative-aws', + config: undefined, + }); + }); + }); + + describe('azure-openai', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.azureOpenAI(); + expect(config).toEqual< + ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> + >({ + name: 'generative-azure-openai', + config: { model: '', isAzure: true }, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.azureOpenAI({ + baseURL: 'http://localhost:8080', + model: 'model', + stop: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> + >({ + name: 'generative-azure-openai', + config: { + baseUrl: 'http://localhost:8080', + stop: { values: ['a', 'b', 'c'] }, + model: 'model', + isAzure: true, + }, + }); + }); + }); + + describe('cohere', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.cohere(); + expect(config).toEqual< + ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined> + >({ + name: 'generative-cohere', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.cohere({ + baseURL: 'http://localhost:8080', + stopSequences: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined> + >({ + name: 'generative-cohere', + config: { + baseUrl: 'http://localhost:8080', + stopSequences: { values: ['a', 'b', 'c'] }, + }, + }); + }); + }); + + describe('databricks', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.databricks(); + expect(config).toEqual< + ModuleConfig< + 'generative-databricks', + GenerativeConfigRuntimeType<'generative-databricks'> | undefined + > + >({ + name: 'generative-databricks', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.databricks({ + stop: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig< + 'generative-databricks', + GenerativeConfigRuntimeType<'generative-databricks'> | undefined + > + >({ + name: 'generative-databricks', + config: { + stop: { values: ['a', 'b', 'c'] }, + }, + }); + }); + }); + + describe('friendliai', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.friendliai(); + expect(config).toEqual< + ModuleConfig< + 'generative-friendliai', + GenerativeConfigRuntimeType<'generative-friendliai'> | undefined + > + >({ + name: 'generative-friendliai', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.friendliai({ + baseURL: 'http://localhost:8080', + }); + expect(config).toEqual< + ModuleConfig< + 'generative-friendliai', + GenerativeConfigRuntimeType<'generative-friendliai'> | undefined + > + >({ + name: 'generative-friendliai', + config: { + baseUrl: 'http://localhost:8080', + }, + }); + }); + }); + + describe('mistral', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.mistral(); + expect(config).toEqual< + ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined> + >({ + name: 'generative-mistral', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.mistral({ + baseURL: 'http://localhost:8080', + }); + expect(config).toEqual< + ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined> + >({ + name: 'generative-mistral', + config: { + baseUrl: 'http://localhost:8080', + }, + }); + }); + }); + + describe('nvidia', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.nvidia(); + expect(config).toEqual< + ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined> + >({ + name: 'generative-nvidia', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.nvidia({ + baseURL: 'http://localhost:8080', + }); + expect(config).toEqual< + ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined> + >({ + name: 'generative-nvidia', + config: { + baseUrl: 'http://localhost:8080', + }, + }); + }); + }); + + describe('ollama', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.ollama(); + expect(config).toEqual< + ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined> + >({ + name: 'generative-ollama', + config: undefined, + }); + }); + }); + + describe('openai', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.openAI(); + expect(config).toEqual< + ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> + >({ + name: 'generative-openai', + config: { model: '', isAzure: false }, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.openAI({ + baseURL: 'http://localhost:8080', + model: 'model', + stop: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> + >({ + name: 'generative-openai', + config: { + baseUrl: 'http://localhost:8080', + isAzure: false, + model: 'model', + stop: { values: ['a', 'b', 'c'] }, + }, + }); + }); + }); +}); diff --git a/src/collections/types/generate.ts b/src/collections/types/generate.ts index 31eca2e2..53bf208c 100644 --- a/src/collections/types/generate.ts +++ b/src/collections/types/generate.ts @@ -119,19 +119,50 @@ export type GroupedTask = { imageProperties?: string[]; }; +type omitFields = 'images' | 'imageProperties'; + export type GenerativeConfigRuntime = - | ModuleConfig<'generative-anthropic', GenerativeAnthropicConfigRuntime> - | ModuleConfig<'generative-anyscale', GenerativeAnyscaleConfigRuntime> - | ModuleConfig<'generative-aws', GenerativeAWSConfigRuntime> - | ModuleConfig<'generative-cohere', GenerativeCohereConfigRuntime> - | ModuleConfig<'generative-databricks', GenerativeDatabricksConfigRuntime> - | ModuleConfig<'generative-dummy', GenerativeDummyConfigRuntime> - | ModuleConfig<'generative-friendliai', GenerativeFriendliAIConfigRuntime> - | ModuleConfig<'generative-google', GenerativeGoogleConfigRuntime> - | ModuleConfig<'generative-mistral', GenerativeMistralConfigRuntime> - | ModuleConfig<'generative-nvidia', GenerativeNvidiaConfigRuntime> - | ModuleConfig<'generative-ollama', GenerativeOllamaConfigRuntime> - | ModuleConfig<'generative-openai', GenerativeOpenAIConfigRuntime>; + | ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'>> + | ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'>> + | ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'>> + | ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> + | ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'>> + | ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-databricks'>> + | ModuleConfig<'generative-dummy', GenerativeConfigRuntimeType<'generative-dummy'>> + | ModuleConfig<'generative-friendliai', GenerativeConfigRuntimeType<'generative-friendliai'>> + | ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-google'>> + | ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'>> + | ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'>> + | ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'>> + | ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>>; + +export type GenerativeConfigRuntimeType = G extends 'generative-anthropic' + ? Omit + : G extends 'generative-anyscale' + ? Omit + : G extends 'generative-aws' + ? Omit + : G extends 'generative-azure-openai' + ? Omit & { isAzure: true } + : G extends 'generative-cohere' + ? Omit + : G extends 'generative-databricks' + ? Omit + : G extends 'generative-google' + ? Omit + : G extends 'generative-friendliai' + ? Omit + : G extends 'generative-mistral' + ? Omit + : G extends 'generative-nvidia' + ? Omit + : G extends 'generative-ollama' + ? Omit + : G extends 'generative-openai' + ? Omit & { isAzure?: false } + : G extends 'none' + ? undefined + : Record | undefined; export type GenerativeMetadata = C extends undefined ? never @@ -167,17 +198,117 @@ export type GenerateReturn = | Promise> | Promise>; -type omitFields = 'images' | 'imageProperties'; +export type GenerativeAnthropicConfigRuntime = { + baseURL?: string | undefined; + maxTokens?: number | undefined; + model?: string | undefined; + temperature?: number | undefined; + topK?: number | undefined; + topP?: number | undefined; + stopSequences?: string[] | undefined; +}; + +export type GenerativeAnyscaleConfigRuntime = { + baseURL?: string | undefined; + model?: string | undefined; + temperature?: number | undefined; +}; + +export type GenerativeAWSConfigRuntime = { + model?: string | undefined; + temperature?: number | undefined; + service?: string | undefined; + region?: string | undefined; + endpoint?: string | undefined; + targetModel?: string | undefined; + targetVariant?: string | undefined; +}; + +export type GenerativeCohereConfigRuntime = { + baseURL?: string | undefined; + frequencyPenalty?: number | undefined; + maxTokens?: number | undefined; + model?: string | undefined; + k?: number | undefined; + p?: number | undefined; + presencePenalty?: number | undefined; + stopSequences?: string[] | undefined; + temperature?: number | undefined; +}; + +export type GenerativeDatabricksConfigRuntime = { + endpoint?: string | undefined; + model?: string | undefined; + frequencyPenalty?: number | undefined; + logProbs?: boolean | undefined; + topLogProbs?: number | undefined; + maxTokens?: number | undefined; + n?: number | undefined; + presencePenalty?: number | undefined; + stop?: string[] | undefined; + temperature?: number | undefined; + topP?: number | undefined; +}; -export type GenerativeAnthropicConfigRuntime = Omit; -export type GenerativeAnyscaleConfigRuntime = Omit; -export type GenerativeAWSConfigRuntime = Omit; -export type GenerativeCohereConfigRuntime = Omit; -export type GenerativeDatabricksConfigRuntime = Omit; -export type GenerativeDummyConfigRuntime = Omit; -export type GenerativeFriendliAIConfigRuntime = Omit; -export type GenerativeGoogleConfigRuntime = Omit; -export type GenerativeMistralConfigRuntime = Omit; -export type GenerativeNvidiaConfigRuntime = Omit; -export type GenerativeOllamaConfigRuntime = Omit; -export type GenerativeOpenAIConfigRuntime = Omit; +export type GenerativeDummyConfigRuntime = GenerativeDummyGRPC; + +export type GenerativeFriendliAIConfigRuntime = { + baseURL?: string | undefined; + model?: string | undefined; + maxTokens?: number | undefined; + temperature?: number | undefined; + n?: number | undefined; + topP?: number | undefined; +}; + +export type GenerativeGoogleConfigRuntime = { + frequencyPenalty?: number | undefined; + maxTokens?: number | undefined; + model?: string | undefined; + presencePenalty?: number | undefined; + temperature?: number | undefined; + topK?: number | undefined; + topP?: number | undefined; + stopSequences?: string[] | undefined; + apiEndpoint?: string | undefined; + projectId?: string | undefined; + endpointId?: string | undefined; + region?: string | undefined; +}; + +export type GenerativeMistralConfigRuntime = { + baseURL?: string | undefined; + maxTokens?: number | undefined; + model?: string | undefined; + temperature?: number | undefined; + topP?: number | undefined; +}; + +export type GenerativeNvidiaConfigRuntime = { + baseURL?: string | undefined; + model?: string | undefined; + temperature?: number | undefined; + topP?: number | undefined; + maxTokens?: number | undefined; +}; + +export type GenerativeOllamaConfigRuntime = { + apiEndpoint?: string | undefined; + model?: string | undefined; + temperature?: number | undefined; +}; + +export type GenerativeOpenAIConfigRuntime = { + frequencyPenalty?: number | undefined; + maxTokens?: number | undefined; + model?: string; + n?: number | undefined; + presencePenalty?: number | undefined; + stop?: string[] | undefined; + temperature?: number | undefined; + topP?: number | undefined; + baseURL?: string | undefined; + apiVersion?: string | undefined; + resourceName?: string | undefined; + deploymentId?: string | undefined; +}; From 98a932bbf1baadf1df0bd78be633ca982a77cc71 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 10 Mar 2025 11:50:49 +0000 Subject: [PATCH 07/49] Remove `it.only` from test --- src/collections/generate/integration.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/collections/generate/integration.test.ts b/src/collections/generate/integration.test.ts index 3b6708fd..468e93bf 100644 --- a/src/collections/generate/integration.test.ts +++ b/src/collections/generate/integration.test.ts @@ -460,7 +460,7 @@ maybe('Testing of the collection.generate methods with runtime generative config }); }); - it.only('should generate using a runtime config without search and with extras', async () => { + it('should generate using a runtime config without search and with extras', async () => { const query = () => collection.generate.fetchObjects({ singlePrompt: { @@ -474,7 +474,6 @@ maybe('Testing of the collection.generate methods with runtime generative config metadata: true, }, config: generativeConfigRuntime.openAI({ - model: 'gpt-4o-mini', stop: ['\n'], }), }); From f533aaa6fae21d9a835650e598111bdf4acf4b46 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 17 Mar 2025 10:45:36 +0000 Subject: [PATCH 08/49] Update CI images --- .github/workflows/main.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index cecd2521..554171ab 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -10,9 +10,9 @@ env: WEAVIATE_124: 1.24.26 WEAVIATE_125: 1.25.34 WEAVIATE_126: 1.26.17 - WEAVIATE_127: 1.27.14 - WEAVIATE_128: 1.28.8 - WEAVIATE_129: 1.29.0 + WEAVIATE_127: 1.27.15 + WEAVIATE_128: 1.28.11 + WEAVIATE_129: 1.29.1 WEAVIATE_130: 1.30.0-dev-680e323 concurrency: From c5c93d9d1e2d57413ec9d6e3687b7bade5377e9a Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 17 Mar 2025 10:45:49 +0000 Subject: [PATCH 09/49] Update name of version checker method --- src/collections/generate/index.ts | 16 ++++++++-------- src/collections/query/check.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/collections/generate/index.ts b/src/collections/generate/index.ts index 3ebcff77..799313f8 100644 --- a/src/collections/generate/index.ts +++ b/src/collections/generate/index.ts @@ -72,7 +72,7 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: FetchObjectsOptions ): Promise> { - return Promise.all([this.check.fetchObjects(opts), this.check.supportForSingleGrouped()]) + return Promise.all([this.check.fetchObjects(opts), this.check.supportForSingleGroupedGenerative()]) .then(async ([{ search }, supportsSingleGrouped]) => search.withFetch({ ...Serialize.search.fetchObjects(opts), @@ -97,7 +97,7 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: Bm25Options ): GenerateReturn { - return Promise.all([this.check.bm25(opts), this.check.supportForSingleGrouped()]) + return Promise.all([this.check.bm25(opts), this.check.supportForSingleGroupedGenerative()]) .then(async ([{ search }, supportsSingleGrouped]) => search.withBm25({ ...Serialize.search.bm25(query, opts), @@ -122,7 +122,7 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: HybridOptions ): GenerateReturn { - return Promise.all([this.check.hybridSearch(opts), this.check.supportForSingleGrouped()]) + return Promise.all([this.check.hybridSearch(opts), this.check.supportForSingleGroupedGenerative()]) .then( async ([ { search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }, @@ -159,7 +159,7 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGrouped()]) + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) .then(([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => Promise.all([ toBase64FromMedia(image), @@ -196,7 +196,7 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGrouped()]) + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) .then(async ([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => search.withNearObject({ ...Serialize.search.nearObject( @@ -228,7 +228,7 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGrouped()]) + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) .then(async ([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => search.withNearText({ ...Serialize.search.nearText( @@ -260,7 +260,7 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearVector(vector, opts), this.check.supportForSingleGrouped()]) + return Promise.all([this.check.nearVector(vector, opts), this.check.supportForSingleGroupedGenerative()]) .then( async ([ { search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }, @@ -300,7 +300,7 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGrouped()]) + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) .then(([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => { const args = { supportsTargets, diff --git a/src/collections/query/check.ts b/src/collections/query/check.ts index 81084f66..ebe87835 100644 --- a/src/collections/query/check.ts +++ b/src/collections/query/check.ts @@ -98,7 +98,7 @@ export class Check { return check.supports; }; - public supportForSingleGrouped = async () => { + public supportForSingleGroupedGenerative = async () => { const check = await this.dbVersionSupport.supportsSingleGrouped(); if (!check.supports) throw new WeaviateUnsupportedFeatureError(check.message); return check.supports; From 72f1f22aef1f6f9e4eb01b109b02ffced898b1fa Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 17 Mar 2025 10:51:18 +0000 Subject: [PATCH 10/49] Update to use latest proto with optional model fix in openai --- src/collections/generate/config.ts | 10 ++++------ src/proto/v1/generative.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/collections/generate/config.ts b/src/collections/generate/config.ts index a633bacc..1ab12b04 100644 --- a/src/collections/generate/config.ts +++ b/src/collections/generate/config.ts @@ -88,18 +88,17 @@ export const generativeConfigRuntime = { azureOpenAI: ( config?: GenerativeOpenAIConfigRuntime ): ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> => { - const { baseURL, model, stop, ...rest } = config || {}; + const { baseURL, stop, ...rest } = config || {}; return { name: 'generative-azure-openai', config: config ? { ...rest, baseUrl: baseURL, - model: model ?? '', isAzure: true, stop: TextArray.fromPartial({ values: stop }), } - : { model: '', isAzure: true }, + : { isAzure: true }, }; }, /** @@ -243,18 +242,17 @@ export const generativeConfigRuntime = { openAI: ( config?: GenerativeOpenAIConfigRuntime ): ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> => { - const { baseURL, model, stop, ...rest } = config || {}; + const { baseURL, stop, ...rest } = config || {}; return { name: 'generative-openai', config: config ? { ...rest, baseUrl: baseURL, - model: model ?? '', isAzure: false, stop: TextArray.fromPartial({ values: stop }), } - : { model: '', isAzure: false }, + : { isAzure: false }, }; }, /** diff --git a/src/proto/v1/generative.ts b/src/proto/v1/generative.ts index 2abae4ba..fe0805fa 100644 --- a/src/proto/v1/generative.ts +++ b/src/proto/v1/generative.ts @@ -118,7 +118,7 @@ export interface GenerativeOllama { export interface GenerativeOpenAI { frequencyPenalty?: number | undefined; maxTokens?: number | undefined; - model: string; + model?: string | undefined; n?: number | undefined; presencePenalty?: number | undefined; stop?: TextArray | undefined; @@ -1884,7 +1884,7 @@ function createBaseGenerativeOpenAI(): GenerativeOpenAI { return { frequencyPenalty: undefined, maxTokens: undefined, - model: "", + model: undefined, n: undefined, presencePenalty: undefined, stop: undefined, @@ -1908,7 +1908,7 @@ export const GenerativeOpenAI = { if (message.maxTokens !== undefined) { writer.uint32(16).int64(message.maxTokens); } - if (message.model !== "") { + if (message.model !== undefined) { writer.uint32(26).string(message.model); } if (message.n !== undefined) { @@ -2075,7 +2075,7 @@ export const GenerativeOpenAI = { return { frequencyPenalty: isSet(object.frequencyPenalty) ? globalThis.Number(object.frequencyPenalty) : undefined, maxTokens: isSet(object.maxTokens) ? globalThis.Number(object.maxTokens) : undefined, - model: isSet(object.model) ? globalThis.String(object.model) : "", + model: isSet(object.model) ? globalThis.String(object.model) : undefined, n: isSet(object.n) ? globalThis.Number(object.n) : undefined, presencePenalty: isSet(object.presencePenalty) ? globalThis.Number(object.presencePenalty) : undefined, stop: isSet(object.stop) ? TextArray.fromJSON(object.stop) : undefined, @@ -2099,7 +2099,7 @@ export const GenerativeOpenAI = { if (message.maxTokens !== undefined) { obj.maxTokens = Math.round(message.maxTokens); } - if (message.model !== "") { + if (message.model !== undefined) { obj.model = message.model; } if (message.n !== undefined) { @@ -2148,7 +2148,7 @@ export const GenerativeOpenAI = { const message = createBaseGenerativeOpenAI(); message.frequencyPenalty = object.frequencyPenalty ?? undefined; message.maxTokens = object.maxTokens ?? undefined; - message.model = object.model ?? ""; + message.model = object.model ?? undefined; message.n = object.n ?? undefined; message.presencePenalty = object.presencePenalty ?? undefined; message.stop = (object.stop !== undefined && object.stop !== null) ? TextArray.fromPartial(object.stop) : undefined; From 1a11acd5984557ab6253fbf50e2b18fa4aca1548 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 17 Mar 2025 11:51:46 +0000 Subject: [PATCH 11/49] Fix unit test --- src/collections/generate/unit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/collections/generate/unit.test.ts b/src/collections/generate/unit.test.ts index e2e296f0..63ff17b9 100644 --- a/src/collections/generate/unit.test.ts +++ b/src/collections/generate/unit.test.ts @@ -74,7 +74,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> >({ name: 'generative-azure-openai', - config: { model: '', isAzure: true }, + config: { isAzure: true }, }); }); it('with values', () => { @@ -255,7 +255,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> >({ name: 'generative-openai', - config: { model: '', isAzure: false }, + config: { isAzure: false }, }); }); it('with values', () => { From fcc34b420754e550743c256204d09ced2f9a1c65 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 17 Mar 2025 13:43:40 +0000 Subject: [PATCH 12/49] Update CI images --- .github/workflows/main.yaml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b30fa219..e5585e34 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -8,11 +8,12 @@ on: env: WEAVIATE_124: 1.24.26 - WEAVIATE_125: 1.25.30 - WEAVIATE_126: 1.26.14 - WEAVIATE_127: 1.27.11 - WEAVIATE_128: 1.28.4 - WEAVIATE_129: 1.29.0 + WEAVIATE_125: 1.25.34 + WEAVIATE_126: 1.26.17 + WEAVIATE_127: 1.27.15 + WEAVIATE_128: 1.28.11 + WEAVIATE_129: 1.29.1 + WEAVIATE_130: 1.30.0-dev-9df35ca jobs: checks: @@ -41,9 +42,10 @@ jobs: { node: "22.x", weaviate: $WEAVIATE_126}, { node: "22.x", weaviate: $WEAVIATE_127}, { node: "22.x", weaviate: $WEAVIATE_128}, - { node: "18.x", weaviate: $WEAVIATE_129}, - { node: "20.x", weaviate: $WEAVIATE_129}, - { node: "22.x", weaviate: $WEAVIATE_129} + { node: "22.x", weaviate: $WEAVIATE_129}, + { node: "18.x", weaviate: $WEAVIATE_130}, + { node: "20.x", weaviate: $WEAVIATE_130}, + { node: "22.x", weaviate: $WEAVIATE_130} ] steps: - uses: actions/checkout@v3 @@ -74,7 +76,7 @@ jobs: fail-fast: false matrix: versions: [ - { node: "22.x", weaviate: $WEAVIATE_129} + { node: "22.x", weaviate: $WEAVIATE_130} ] steps: - uses: actions/checkout@v3 From 402f64a4b5b9462da9962a1af352b7d3e58a461d Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 17 Mar 2025 13:45:47 +0000 Subject: [PATCH 13/49] merge --- .github/workflows/main.yaml | 4 + src/collections/config/types/generative.ts | 7 + src/collections/configure/generative.ts | 18 ++ src/collections/configure/types/generative.ts | 6 + src/collections/deserialize/index.ts | 36 ++- src/collections/generate/config.ts | 280 ++++++++++++++++ src/collections/generate/index.ts | 284 +++++++++-------- src/collections/generate/integration.test.ts | 115 ++++++- src/collections/generate/types.ts | 186 ++++++----- src/collections/generate/unit.test.ts | 280 ++++++++++++++++ src/collections/query/check.ts | 6 + src/collections/serialize/index.ts | 136 +++++++- src/collections/serialize/unit.test.ts | 15 +- src/collections/types/generate.ts | 300 ++++++++++++++++-- src/grpc/searcher.ts | 1 + src/proto/v1/generative.ts | 12 +- src/utils/dbVersion.ts | 11 + 17 files changed, 1437 insertions(+), 260 deletions(-) create mode 100644 src/collections/generate/config.ts create mode 100644 src/collections/generate/unit.test.ts diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e5585e34..1d5b1a0b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,6 +15,10 @@ env: WEAVIATE_129: 1.29.1 WEAVIATE_130: 1.30.0-dev-9df35ca +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: checks: runs-on: ubuntu-latest diff --git a/src/collections/config/types/generative.ts b/src/collections/config/types/generative.ts index 667bc347..7ff426b5 100644 --- a/src/collections/config/types/generative.ts +++ b/src/collections/config/types/generative.ts @@ -58,6 +58,13 @@ export type GenerativeMistralConfig = { temperature?: number; }; +export type GenerativeNvidiaConfig = { + baseURL?: string; + maxTokens?: number; + model?: string; + temperature?: number; +}; + export type GenerativeOllamaConfig = { apiEndpoint?: string; model?: string; diff --git a/src/collections/configure/generative.ts b/src/collections/configure/generative.ts index 730f2bcb..d4ee3154 100644 --- a/src/collections/configure/generative.ts +++ b/src/collections/configure/generative.ts @@ -8,6 +8,7 @@ import { GenerativeFriendliAIConfig, GenerativeGoogleConfig, GenerativeMistralConfig, + GenerativeNvidiaConfig, GenerativeOllamaConfig, GenerativeOpenAIConfig, GenerativePaLMConfig, @@ -22,6 +23,7 @@ import { GenerativeDatabricksConfigCreate, GenerativeFriendliAIConfigCreate, GenerativeMistralConfigCreate, + GenerativeNvidiaConfigCreate, GenerativeOllamaConfigCreate, GenerativeOpenAIConfigCreate, GenerativePaLMConfigCreate, @@ -169,6 +171,22 @@ export default { config, }; }, + /** + * Create a `ModuleConfig<'generative-nvidia', GenerativeNvidiaConfig | undefined>` object for use when performing AI generation using the `generative-mistral` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/nvidia/generative) for detailed usage. + * + * @param {GenerativeNvidiaConfigCreate} [config] The configuration for the `generative-nvidia` module. + * @returns {ModuleConfig<'generative-nvidia', GenerativeNvidiaConfig | undefined>} The configuration object. + */ + nvidia( + config?: GenerativeNvidiaConfigCreate + ): ModuleConfig<'generative-nvidia', GenerativeNvidiaConfig | undefined> { + return { + name: 'generative-nvidia', + config, + }; + }, /** * Create a `ModuleConfig<'generative-ollama', GenerativeOllamaConfig | undefined>` object for use when performing AI generation using the `generative-ollama` module. * diff --git a/src/collections/configure/types/generative.ts b/src/collections/configure/types/generative.ts index 2b1a18cf..ccf22ec6 100644 --- a/src/collections/configure/types/generative.ts +++ b/src/collections/configure/types/generative.ts @@ -5,6 +5,7 @@ import { GenerativeDatabricksConfig, GenerativeFriendliAIConfig, GenerativeMistralConfig, + GenerativeNvidiaConfig, GenerativeOllamaConfig, GenerativePaLMConfig, } from '../../index.js'; @@ -44,6 +45,8 @@ export type GenerativeFriendliAIConfigCreate = GenerativeFriendliAIConfig; export type GenerativeMistralConfigCreate = GenerativeMistralConfig; +export type GenerativeNvidiaConfigCreate = GenerativeNvidiaConfig; + export type GenerativeOllamaConfigCreate = GenerativeOllamaConfig; export type GenerativeOpenAIConfigCreate = GenerativeOpenAIConfigBaseCreate & { @@ -61,6 +64,7 @@ export type GenerativeConfigCreate = | GenerativeDatabricksConfigCreate | GenerativeFriendliAIConfigCreate | GenerativeMistralConfigCreate + | GenerativeNvidiaConfigCreate | GenerativeOllamaConfigCreate | GenerativeOpenAIConfigCreate | GenerativePaLMConfigCreate @@ -81,6 +85,8 @@ export type GenerativeConfigCreateType = G extends 'generative-anthropic' ? GenerativeFriendliAIConfigCreate : G extends 'generative-mistral' ? GenerativeMistralConfigCreate + : G extends 'generative-nvidia' + ? GenerativeNvidiaConfigCreate : G extends 'generative-ollama' ? GenerativeOllamaConfigCreate : G extends 'generative-openai' diff --git a/src/collections/deserialize/index.ts b/src/collections/deserialize/index.ts index 588c2642..41f9beff 100644 --- a/src/collections/deserialize/index.ts +++ b/src/collections/deserialize/index.ts @@ -25,6 +25,8 @@ import { AggregateResult, AggregateText, AggregateType, + GenerativeConfigRuntime, + GenerativeMetadata, PropertiesMetrics, } from '../index.js'; import { referenceFromObjects } from '../references/utils.js'; @@ -207,11 +209,24 @@ export class Deserialize { }; } - public generate(reply: SearchReply): GenerativeReturn { + public generate( + reply: SearchReply + ): GenerativeReturn { return { objects: reply.results.map((result) => { return { - generated: result.metadata?.generativePresent ? result.metadata?.generative : undefined, + generated: result.metadata?.generativePresent + ? result.metadata?.generative + : result.generative + ? result.generative.values[0].result + : undefined, + generative: result.generative + ? { + text: result.generative.values[0].result, + debug: result.generative.values[0].debug, + metadata: result.generative.values[0].metadata as GenerativeMetadata, + } + : undefined, metadata: Deserialize.metadata(result.metadata), properties: this.properties(result.properties), references: this.references(result.properties), @@ -219,7 +234,18 @@ export class Deserialize { vectors: Deserialize.vectors(result.metadata), } as any; }), - generated: reply.generativeGroupedResult, + generated: + reply.generativeGroupedResult !== '' + ? reply.generativeGroupedResult + : reply.generativeGroupedResults + ? reply.generativeGroupedResults.values[0].result + : undefined, + generative: reply.generativeGroupedResults + ? { + text: reply.generativeGroupedResults?.values[0].result, + metadata: reply.generativeGroupedResults?.values[0].metadata as GenerativeMetadata, + } + : undefined, }; } @@ -252,9 +278,9 @@ export class Deserialize { }; } - public generateGroupBy(reply: SearchReply): GenerativeGroupByReturn { + public generateGroupBy(reply: SearchReply): GenerativeGroupByReturn { const objects: GroupByObject[] = []; - const groups: Record> = {}; + const groups: Record> = {}; reply.groupByResults.forEach((result) => { const objs = result.objects.map((object) => { return { diff --git a/src/collections/generate/config.ts b/src/collections/generate/config.ts new file mode 100644 index 00000000..1ab12b04 --- /dev/null +++ b/src/collections/generate/config.ts @@ -0,0 +1,280 @@ +import { TextArray } from '../../proto/v1/base.js'; +import { ModuleConfig } from '../config/types/index.js'; +import { + GenerativeAWSConfigRuntime, + GenerativeAnthropicConfigRuntime, + GenerativeAnyscaleConfigRuntime, + GenerativeCohereConfigRuntime, + GenerativeConfigRuntimeType, + GenerativeDatabricksConfigRuntime, + GenerativeFriendliAIConfigRuntime, + GenerativeGoogleConfigRuntime, + GenerativeMistralConfigRuntime, + GenerativeNvidiaConfigRuntime, + GenerativeOllamaConfigRuntime, + GenerativeOpenAIConfigRuntime, +} from '../index.js'; + +export const generativeConfigRuntime = { + /** + * Create a `ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-anthropic` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/anthropic/generative) for detailed usage. + * + * @param {GenerativeAnthropicConfigCreateRuntime} [config] The configuration for the `generative-anthropic` module. + * @returns {ModuleConfig<'generative-anthropic', GenerativeAnthropicConfigCreateRuntime | undefined>} The configuration object. + */ + anthropic( + config?: GenerativeAnthropicConfigRuntime + ): ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined> { + const { baseURL, stopSequences, ...rest } = config || {}; + return { + name: 'generative-anthropic', + config: config + ? { + ...rest, + baseUrl: baseURL, + stopSequences: TextArray.fromPartial({ values: stopSequences }), + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-anyscale` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/anyscale/generative) for detailed usage. + * + * @param {GenerativeAnyscaleConfigRuntime} [config] The configuration for the `generative-aws` module. + * @returns {ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined>} The configuration object. + */ + anyscale( + config?: GenerativeAnyscaleConfigRuntime + ): ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined> { + const { baseURL, ...rest } = config || {}; + return { + name: 'generative-anyscale', + config: config + ? { + ...rest, + baseUrl: baseURL, + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-aws` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/aws/generative) for detailed usage. + * + * @param {GenerativeAWSConfigRuntime} [config] The configuration for the `generative-aws` module. + * @returns {ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined>} The configuration object. + */ + aws( + config?: GenerativeAWSConfigRuntime + ): ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined> { + return { + name: 'generative-aws', + config, + }; + }, + /** + * Create a `ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>>` object for use when performing runtime-specific AI generation using the `generative-openai` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/openai/generative) for detailed usage. + * + * @param {GenerativeAzureOpenAIConfigRuntime} [config] The configuration for the `generative-openai` module. + * @returns {ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>>} The configuration object. + */ + azureOpenAI: ( + config?: GenerativeOpenAIConfigRuntime + ): ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> => { + const { baseURL, stop, ...rest } = config || {}; + return { + name: 'generative-azure-openai', + config: config + ? { + ...rest, + baseUrl: baseURL, + isAzure: true, + stop: TextArray.fromPartial({ values: stop }), + } + : { isAzure: true }, + }; + }, + /** + * Create a `ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-cohere` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/cohere/generative) for detailed usage. + * + * @param {GenerativeCohereConfigRuntime} [config] The configuration for the `generative-cohere` module. + * @returns {ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined>} The configuration object. + */ + cohere: ( + config?: GenerativeCohereConfigRuntime + ): ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined> => { + const { baseURL, stopSequences, ...rest } = config || {}; + return { + name: 'generative-cohere', + config: config + ? { + ...rest, + baseUrl: baseURL, + stopSequences: TextArray.fromPartial({ values: stopSequences }), + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-databricks'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-databricks` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/databricks/generative) for detailed usage. + * + * @param {GenerativeDatabricksConfigRuntime} [config] The configuration for the `generative-databricks` module. + * @returns {ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-databricks'> | undefined>} The configuration object. + */ + databricks: ( + config?: GenerativeDatabricksConfigRuntime + ): ModuleConfig< + 'generative-databricks', + GenerativeConfigRuntimeType<'generative-databricks'> | undefined + > => { + const { stop, ...rest } = config || {}; + return { + name: 'generative-databricks', + config: config + ? { + ...rest, + stop: TextArray.fromPartial({ values: stop }), + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-friendliai', GenerativeConfigRuntimeType<'generative-friendliai'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-friendliai` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/friendliai/generative) for detailed usage. + * + * @param {GenerativeFriendliAIConfigRuntime} [config] The configuration for the `generative-friendliai` module. + * @returns {ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-friendliai'> | undefined>} The configuration object. + */ + friendliai( + config?: GenerativeFriendliAIConfigRuntime + ): ModuleConfig<'generative-friendliai', GenerativeConfigRuntimeType<'generative-friendliai'> | undefined> { + const { baseURL, ...rest } = config || {}; + return { + name: 'generative-friendliai', + config: config + ? { + ...rest, + baseUrl: baseURL, + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-mistral` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/mistral/generative) for detailed usage. + * + * @param {GenerativeMistralConfigRuntime} [config] The configuration for the `generative-mistral` module. + * @returns {ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined>} The configuration object. + */ + mistral( + config?: GenerativeMistralConfigRuntime + ): ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined> { + const { baseURL, ...rest } = config || {}; + return { + name: 'generative-mistral', + config: config + ? { + baseUrl: baseURL, + ...rest, + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-mistral` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/nvidia/generative) for detailed usage. + * + * @param {GenerativeNvidiaConfigCreate} [config] The configuration for the `generative-nvidia` module. + * @returns {ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined>} The configuration object. + */ + nvidia( + config?: GenerativeNvidiaConfigRuntime + ): ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined> { + const { baseURL, ...rest } = config || {}; + return { + name: 'generative-nvidia', + config: config + ? { + ...rest, + baseUrl: baseURL, + } + : undefined, + }; + }, + /** + * Create a `ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-ollama` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/ollama/generative) for detailed usage. + * + * @param {GenerativeOllamaConfigRuntime} [config] The configuration for the `generative-openai` module. + * @returns {ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined>} The configuration object. + */ + ollama( + config?: GenerativeOllamaConfigRuntime + ): ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined> { + return { + name: 'generative-ollama', + config, + }; + }, + /** + * Create a `ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>>` object for use when performing runtime-specific AI generation using the `generative-openai` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/openai/generative) for detailed usage. + * + * @param {GenerativeOpenAIConfigRuntime} [config] The configuration for the `generative-openai` module. + * @returns {ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>>} The configuration object. + */ + openAI: ( + config?: GenerativeOpenAIConfigRuntime + ): ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> => { + const { baseURL, stop, ...rest } = config || {}; + return { + name: 'generative-openai', + config: config + ? { + ...rest, + baseUrl: baseURL, + isAzure: false, + stop: TextArray.fromPartial({ values: stop }), + } + : { isAzure: false }, + }; + }, + /** + * Create a `ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-openai'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-google` module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/google/generative) for detailed usage. + * + * @param {GenerativeGoogleConfigRuntime} [config] The configuration for the `generative-palm` module. + * @returns {ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-google'> | undefined>} The configuration object. + */ + google: ( + config?: GenerativeGoogleConfigRuntime + ): ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-google'> | undefined> => { + const { stopSequences, ...rest } = config || {}; + return { + name: 'generative-google', + config: config + ? { + ...rest, + stopSequences: TextArray.fromPartial({ values: stopSequences }), + } + : undefined, + }; + }, +}; diff --git a/src/collections/generate/index.ts b/src/collections/generate/index.ts index 3af6fef1..799313f8 100644 --- a/src/collections/generate/index.ts +++ b/src/collections/generate/index.ts @@ -5,6 +5,7 @@ import { DbVersionSupport } from '../../utils/dbVersion.js'; import { WeaviateInvalidInputError } from '../../errors.js'; import { toBase64FromMedia } from '../../index.js'; +import { GenerativeSearch } from '../../proto/v1/generative.js'; import { SearchReply } from '../../proto/v1/search_get.js'; import { Deserialize } from '../deserialize/index.js'; import { Check } from '../query/check.js'; @@ -28,6 +29,7 @@ import { Serialize } from '../serialize/index.js'; import { GenerateOptions, GenerateReturn, + GenerativeConfigRuntime, GenerativeGroupByReturn, GenerativeReturn, GroupByOptions, @@ -51,107 +53,118 @@ class GenerateManager implements Generate { return new GenerateManager(new Check(connection, name, dbVersionSupport, consistencyLevel, tenant)); } - private async parseReply(reply: SearchReply) { + private async parseReply(reply: SearchReply) { const deserialize = await Deserialize.use(this.check.dbVersionSupport); - return deserialize.generate(reply); + return deserialize.generate(reply); } - private async parseGroupByReply( + private async parseGroupByReply( opts: SearchOptions | GroupByOptions | undefined, reply: SearchReply ) { const deserialize = await Deserialize.use(this.check.dbVersionSupport); return Serialize.search.isGroupBy(opts) ? deserialize.generateGroupBy(reply) - : deserialize.generate(reply); + : deserialize.generate(reply); } - public fetchObjects( - generate: GenerateOptions, + public fetchObjects( + generate: GenerateOptions, opts?: FetchObjectsOptions - ): Promise> { - return this.check - .fetchObjects(opts) - .then(({ search }) => + ): Promise> { + return Promise.all([this.check.fetchObjects(opts), this.check.supportForSingleGroupedGenerative()]) + .then(async ([{ search }, supportsSingleGrouped]) => search.withFetch({ ...Serialize.search.fetchObjects(opts), - generative: Serialize.generative(generate), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), }) ) .then((reply) => this.parseReply(reply)); } - public bm25( + public bm25( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseBm25Options - ): Promise>; - public bm25( + ): Promise>; + public bm25( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByBm25Options - ): Promise>; - public bm25(query: string, generate: GenerateOptions, opts?: Bm25Options): GenerateReturn { - return this.check - .bm25(opts) - .then(({ search }) => + ): Promise>; + public bm25( + query: string, + generate: GenerateOptions, + opts?: Bm25Options + ): GenerateReturn { + return Promise.all([this.check.bm25(opts), this.check.supportForSingleGroupedGenerative()]) + .then(async ([{ search }, supportsSingleGrouped]) => search.withBm25({ ...Serialize.search.bm25(query, opts), - generative: Serialize.generative(generate), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public hybrid( + public hybrid( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseHybridOptions - ): Promise>; - public hybrid( + ): Promise>; + public hybrid( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByHybridOptions - ): Promise>; - public hybrid(query: string, generate: GenerateOptions, opts?: HybridOptions): GenerateReturn { - return this.check - .hybridSearch(opts) - .then(({ search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }) => - search.withHybrid({ - ...Serialize.search.hybrid( - { - query, - supportsTargets, - supportsVectorsForTargets, - supportsWeightsForTargets, - }, - opts - ), - generative: Serialize.generative(generate), - }) + ): Promise>; + public hybrid( + query: string, + generate: GenerateOptions, + opts?: HybridOptions + ): GenerateReturn { + return Promise.all([this.check.hybridSearch(opts), this.check.supportForSingleGroupedGenerative()]) + .then( + async ([ + { search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }, + supportsSingleGrouped, + ]) => + search.withHybrid({ + ...Serialize.search.hybrid( + { + query, + supportsTargets, + supportsVectorsForTargets, + supportsWeightsForTargets, + }, + opts + ), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), + }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearImage( + public nearImage( image: string | Buffer, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; - public nearImage( + ): Promise>; + public nearImage( image: string | Buffer, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; - public nearImage( + ): Promise>; + public nearImage( image: string | Buffer, - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn { - return this.check - .nearSearch(opts) - .then(({ search, supportsTargets, supportsWeightsForTargets }) => - toBase64FromMedia(image).then((image) => + ): GenerateReturn { + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) + .then(([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => + Promise.all([ + toBase64FromMedia(image), + Serialize.generative({ supportsSingleGrouped }, generate), + ]).then(([image, generative]) => search.withNearImage({ ...Serialize.search.nearImage( { @@ -161,27 +174,30 @@ class GenerateManager implements Generate { }, opts ), - generative: Serialize.generative(generate), + generative, }) ) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearObject( + public nearObject( id: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; - public nearObject( + ): Promise>; + public nearObject( id: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; - public nearObject(id: string, generate: GenerateOptions, opts?: NearOptions): GenerateReturn { - return this.check - .nearSearch(opts) - .then(({ search, supportsTargets, supportsWeightsForTargets }) => + ): Promise>; + public nearObject( + id: string, + generate: GenerateOptions, + opts?: NearOptions + ): GenerateReturn { + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) + .then(async ([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => search.withNearObject({ ...Serialize.search.nearObject( { @@ -191,30 +207,29 @@ class GenerateManager implements Generate { }, opts ), - generative: Serialize.generative(generate), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearText( + public nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearTextOptions - ): Promise>; - public nearText( + ): Promise>; + public nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearTextOptions - ): Promise>; - public nearText( + ): Promise>; + public nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn { - return this.check - .nearSearch(opts) - .then(({ search, supportsTargets, supportsWeightsForTargets }) => + ): GenerateReturn { + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) + .then(async ([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => search.withNearText({ ...Serialize.search.nearText( { @@ -224,114 +239,124 @@ class GenerateManager implements Generate { }, opts ), - generative: Serialize.generative(generate), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearVector( + public nearVector( vector: number[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; - public nearVector( + ): Promise>; + public nearVector( vector: number[], - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; - public nearVector( + ): Promise>; + public nearVector( vector: number[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn { - return this.check - .nearVector(vector, opts) - .then(({ search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }) => - search.withNearVector({ - ...Serialize.search.nearVector( - { - vector, - supportsTargets, - supportsVectorsForTargets, - supportsWeightsForTargets, - }, - opts - ), - generative: Serialize.generative(generate), - }) + ): GenerateReturn { + return Promise.all([this.check.nearVector(vector, opts), this.check.supportForSingleGroupedGenerative()]) + .then( + async ([ + { search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }, + supportsSingleGrouped, + ]) => + search.withNearVector({ + ...Serialize.search.nearVector( + { + vector, + supportsTargets, + supportsVectorsForTargets, + supportsWeightsForTargets, + }, + opts + ), + generative: await Serialize.generative({ supportsSingleGrouped }, generate), + }) ) .then((reply) => this.parseGroupByReply(opts, reply)); } - public nearMedia( + public nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; - public nearMedia( + ): Promise>; + public nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; - public nearMedia( + ): Promise>; + public nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn { - return this.check - .nearSearch(opts) - .then(({ search, supportsTargets, supportsWeightsForTargets }) => { + ): GenerateReturn { + return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) + .then(([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => { const args = { supportsTargets, supportsWeightsForTargets, }; - const generative = Serialize.generative(generate); - let send: (media: string) => Promise; + let send: (media: string, generative: GenerativeSearch) => Promise; switch (type) { case 'audio': - send = (media) => + send = (media, generative) => search.withNearAudio({ ...Serialize.search.nearAudio({ audio: media, ...args }, opts), generative, }); break; case 'depth': - send = (media) => + send = (media, generative) => search.withNearDepth({ ...Serialize.search.nearDepth({ depth: media, ...args }, opts), generative, }); break; case 'image': - send = (media) => + send = (media, generative) => search.withNearImage({ ...Serialize.search.nearImage({ image: media, ...args }, opts), generative, }); break; case 'imu': - send = (media) => - search.withNearIMU({ ...Serialize.search.nearIMU({ imu: media, ...args }, opts), generative }); + send = (media, generative) => + search.withNearIMU({ + ...Serialize.search.nearIMU({ imu: media, ...args }, opts), + generative, + }); break; case 'thermal': - send = (media) => + send = (media, generative) => search.withNearThermal({ ...Serialize.search.nearThermal({ thermal: media, ...args }, opts), generative, }); break; case 'video': - send = (media) => - search.withNearVideo({ ...Serialize.search.nearVideo({ video: media, ...args }), generative }); + send = (media, generative) => + search.withNearVideo({ + ...Serialize.search.nearVideo({ video: media, ...args }), + generative, + }); break; default: throw new WeaviateInvalidInputError(`Invalid media type: ${type}`); } - return toBase64FromMedia(media).then(send); + return Promise.all([ + toBase64FromMedia(media), + Serialize.generative({ supportsSingleGrouped }, generate), + ]).then(([media, generative]) => send(media, generative)); }) .then((reply) => this.parseGroupByReply(opts, reply)); } @@ -339,4 +364,5 @@ class GenerateManager implements Generate { export default GenerateManager.use; +export { generativeConfigRuntime } from './config.js'; export { Generate } from './types.js'; diff --git a/src/collections/generate/integration.test.ts b/src/collections/generate/integration.test.ts index 41846f90..6e941d96 100644 --- a/src/collections/generate/integration.test.ts +++ b/src/collections/generate/integration.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ import { WeaviateUnsupportedFeatureError } from '../../errors.js'; -import weaviate, { WeaviateClient } from '../../index.js'; +import weaviate, { WeaviateClient, generativeConfigRuntime } from '../../index.js'; import { Collection } from '../collection/index.js'; import { GenerateOptions, GroupByOptions } from '../types/index.js'; @@ -27,10 +27,10 @@ maybe('Testing of the collection.generate methods with a simple collection', () testProp: string; }; - const generateOpts: GenerateOptions = { + const generateOpts = { singlePrompt: 'Write a haiku about ducks for {testProp}', groupedTask: 'What is the value of testProp here?', - groupedProperties: ['testProp'], + groupedProperties: ['testProp'] as 'testProp'[], }; afterAll(() => { @@ -162,7 +162,7 @@ maybe('Testing of the groupBy collection.generate methods with a simple collecti testProp: string; }; - const generateOpts: GenerateOptions = { + const generateOpts: GenerateOptions = { singlePrompt: 'Write a haiku about ducks for {testProp}', groupedTask: 'What is the value of testProp here?', groupedProperties: ['testProp'], @@ -421,3 +421,110 @@ maybe('Testing of the collection.generate methods with a multi vector collection expect(ret.objects[1].generated).toBeDefined(); }); }); + +maybe('Testing of the collection.generate methods with runtime generative config', () => { + let client: WeaviateClient; + let collection: Collection; + const collectionName = 'TestCollectionGenerateConfigRuntime'; + + type TestCollectionGenerateConfigRuntime = { + testProp: string; + }; + + afterAll(() => { + return client.collections.delete(collectionName).catch((err) => { + console.error(err); + throw err; + }); + }); + + beforeAll(async () => { + client = await makeOpenAIClient(); + collection = client.collections.get(collectionName); + return client.collections + .create({ + name: collectionName, + properties: [ + { + name: 'testProp', + dataType: 'text', + }, + ], + }) + .then(() => { + return collection.data.insert({ + properties: { + testProp: 'test', + }, + }); + }); + }); + + it('should generate using a runtime config without search and with extras', async () => { + const query = () => + collection.generate.fetchObjects({ + singlePrompt: { + prompt: 'Write a haiku about ducks for {testProp}', + debug: true, + metadata: true, + }, + groupedTask: { + prompt: 'What is the value of testProp here?', + nonBlobProperties: ['testProp'], + metadata: true, + }, + config: generativeConfigRuntime.openAI({ + stop: ['\n'], + }), + }); + + if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 30, 0))) { + await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); + return; + } + + const res = await query(); + expect(res.objects.length).toEqual(1); + expect(res.generated).toBeDefined(); + expect(res.generative?.text).toBeDefined(); + expect(res.generative?.metadata).toBeDefined(); + res.objects.forEach((obj) => { + expect(obj.generated).toBeDefined(); + expect(obj.generative?.text).toBeDefined(); + expect(obj.generative?.metadata).toBeDefined(); + expect(obj.generative?.debug).toBeDefined(); + }); + }); + + it('should generate using a runtime config without search nor extras', async () => { + const query = () => + collection.generate.fetchObjects({ + singlePrompt: 'Write a haiku about ducks for {testProp}', + groupedTask: 'What is the value of testProp here?', + config: { + name: 'generative-openai', + config: { + model: 'gpt-4o-mini', + stop: { values: ['\n'] }, + }, + }, + }); + + if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 30, 0))) { + await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); + return; + } + + const res = await query(); + expect(res.objects.length).toEqual(1); + expect(res.generated).toBeDefined(); + expect(res.generative?.text).toBeDefined(); + expect(res.generative?.metadata).toBeUndefined(); + res.objects.forEach((obj) => { + expect(obj.generated).toBeDefined(); + expect(obj.generative?.text).toBeDefined(); + expect(obj.generative?.metadata).toBeUndefined(); + expect(obj.generative?.debug).toBeUndefined(); + }); + }); +}); diff --git a/src/collections/generate/types.ts b/src/collections/generate/types.ts index b211a46a..27548bfb 100644 --- a/src/collections/generate/types.ts +++ b/src/collections/generate/types.ts @@ -18,6 +18,7 @@ import { import { GenerateOptions, GenerateReturn, + GenerativeConfigRuntime, GenerativeGroupByReturn, GenerativeReturn, } from '../types/index.js'; @@ -31,11 +32,15 @@ interface Bm25 { * This overload is for performing a search without the `groupBy` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseBm25Options} [opts] - The available options for performing the BM25 search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - bm25(query: string, generate: GenerateOptions, opts?: BaseBm25Options): Promise>; + bm25( + query: string, + generate: GenerateOptions, + opts?: BaseBm25Options + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a keyword-based BM25 search of objects in this collection. * @@ -44,15 +49,15 @@ interface Bm25 { * This overload is for performing a search with the `groupBy` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByBm25Options} opts - The available options for performing the BM25 search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - bm25( + bm25( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByBm25Options - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a keyword-based BM25 search of objects in this collection. * @@ -61,11 +66,15 @@ interface Bm25 { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {Bm25Options} [opts] - The available options for performing the BM25 search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - bm25(query: string, generate: GenerateOptions, opts?: Bm25Options): GenerateReturn; + bm25( + query: string, + generate: GenerateOptions, + opts?: Bm25Options + ): GenerateReturn; } interface Hybrid { @@ -77,15 +86,15 @@ interface Hybrid { * This overload is for performing a search without the `groupBy` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseHybridOptions} [opts] - The available options for performing the hybrid search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - hybrid( + hybrid( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseHybridOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of an object search in this collection using the hybrid algorithm blending keyword-based BM25 and vector-based similarity. * @@ -94,15 +103,15 @@ interface Hybrid { * This overload is for performing a search with the `groupBy` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByHybridOptions} opts - The available options for performing the hybrid search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - hybrid( + hybrid( query: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByHybridOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of an object search in this collection using the hybrid algorithm blending keyword-based BM25 and vector-based similarity. * @@ -111,11 +120,15 @@ interface Hybrid { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {string} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {HybridOptions} [opts] - The available options for performing the hybrid search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - hybrid(query: string, generate: GenerateOptions, opts?: HybridOptions): GenerateReturn; + hybrid( + query: string, + generate: GenerateOptions, + opts?: HybridOptions + ): GenerateReturn; } interface NearMedia { @@ -130,16 +143,16 @@ interface NearMedia { * * @param {string | Buffer} media - The media file to search on. This can be a base64 string, a file path string, or a buffer. * @param {NearMediaType} type - The type of media to search on. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseNearOptions} [opts] - The available options for performing the near-media search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - nearMedia( + nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-audio object search in this collection using an audio-capable vectorization module and vector-based similarity search. * @@ -151,16 +164,16 @@ interface NearMedia { * * @param {string | Buffer} media - The media file to search on. This can be a base64 string, a file path string, or a buffer. * @param {NearMediaType} type - The type of media to search on. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByNearOptions} opts - The available options for performing the near-media search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - nearMedia( + nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-audio object search in this collection using an audio-capable vectorization module and vector-based similarity search. * @@ -172,16 +185,16 @@ interface NearMedia { * * @param {string | Buffer} media - The media to search on. This can be a base64 string, a file path string, or a buffer. * @param {NearMediaType} type - The type of media to search on. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {NearOptions} [opts] - The available options for performing the near-media search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - nearMedia( + nearMedia( media: string | Buffer, type: NearMediaType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn; + ): GenerateReturn; } interface NearObject { @@ -193,15 +206,15 @@ interface NearObject { * This overload is for performing a search without the `groupBy` param. * * @param {string} id - The ID of the object to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseNearOptions} [opts] - The available options for performing the near-object search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - nearObject( + nearObject( id: string, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-object object search in this collection using a vector-based similarity search. * @@ -210,15 +223,15 @@ interface NearObject { * This overload is for performing a search with the `groupBy` param. * * @param {string} id - The ID of the object to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByNearOptions} opts - The available options for performing the near-object search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - nearObject( + nearObject( id: string, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-object object search in this collection using a vector-based similarity search. * @@ -227,11 +240,15 @@ interface NearObject { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {string} id - The ID of the object to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {NearOptions} [opts] - The available options for performing the near-object search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - nearObject(id: string, generate: GenerateOptions, opts?: NearOptions): GenerateReturn; + nearObject( + id: string, + generate: GenerateOptions, + opts?: NearOptions + ): GenerateReturn; } interface NearText { @@ -245,15 +262,15 @@ interface NearText { * This overload is for performing a search without the `groupBy` param. * * @param {string | string[]} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseNearTextOptions} [opts] - The available options for performing the near-text search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - nearText( + nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearTextOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-image object search in this collection using the image-capable vectorization module and vector-based similarity search. * @@ -264,15 +281,15 @@ interface NearText { * This overload is for performing a search with the `groupBy` param. * * @param {string | string[]} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByNearTextOptions} opts - The available options for performing the near-text search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - nearText( + nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearTextOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-image object search in this collection using the image-capable vectorization module and vector-based similarity search. * @@ -283,15 +300,15 @@ interface NearText { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {string | string[]} query - The query to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {NearTextOptions} [opts] - The available options for performing the near-text search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - nearText( + nearText( query: string | string[], - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearTextOptions - ): GenerateReturn; + ): GenerateReturn; } interface NearVector { @@ -303,15 +320,15 @@ interface NearVector { * This overload is for performing a search without the `groupBy` param. * * @param {NearVectorInputType} vector - The vector(s) to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {BaseNearOptions} [opts] - The available options for performing the near-vector search. - * @return {Promise>} - The results of the search including the generated data. + * @return {Promise>} - The results of the search including the generated data. */ - nearVector( + nearVector( vector: NearVectorInputType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: BaseNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-vector object search in this collection using vector-based similarity search. * @@ -320,15 +337,15 @@ interface NearVector { * This overload is for performing a search with the `groupBy` param. * * @param {NearVectorInputType} vector - The vector(s) to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {GroupByNearOptions} opts - The available options for performing the near-vector search. - * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. + * @return {Promise>} - The results of the search including the generated data grouped by the specified properties. */ - nearVector( + nearVector( vector: NearVectorInputType, - generate: GenerateOptions, + generate: GenerateOptions, opts: GroupByNearOptions - ): Promise>; + ): Promise>; /** * Perform retrieval-augmented generation (RaG) on the results of a by-vector object search in this collection using vector-based similarity search. * @@ -337,15 +354,15 @@ interface NearVector { * This overload is for performing a search with a programmatically defined `opts` param. * * @param {NearVectorInputType} vector - The vector(s) to search for. - * @param {GenerateOptions} generate - The available options for performing the generation. + * @param {GenerateOptions} generate - The available options for performing the generation. * @param {NearOptions} [opts] - The available options for performing the near-vector search. - * @return {GenerateReturn} - The results of the search including the generated data. + * @return {GenerateReturn} - The results of the search including the generated data. */ - nearVector( + nearVector( vector: NearVectorInputType, - generate: GenerateOptions, + generate: GenerateOptions, opts?: NearOptions - ): GenerateReturn; + ): GenerateReturn; } export interface Generate @@ -355,5 +372,8 @@ export interface Generate NearObject, NearText, NearVector { - fetchObjects: (generate: GenerateOptions, opts?: FetchObjectsOptions) => Promise>; + fetchObjects: ( + generate: GenerateOptions, + opts?: FetchObjectsOptions + ) => Promise>; } diff --git a/src/collections/generate/unit.test.ts b/src/collections/generate/unit.test.ts new file mode 100644 index 00000000..63ff17b9 --- /dev/null +++ b/src/collections/generate/unit.test.ts @@ -0,0 +1,280 @@ +import { GenerativeConfigRuntimeType, ModuleConfig } from '../types'; +import { generativeConfigRuntime } from './config'; + +// only tests fields that must be mapped from some public name to a gRPC name, e.g. baseURL -> baseUrl and stop: string[] -> stop: TextArray +describe('Unit testing of the generativeConfigRuntime factory methods', () => { + describe('anthropic', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.anthropic(); + expect(config).toEqual< + ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined> + >({ + name: 'generative-anthropic', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.anthropic({ + baseURL: 'http://localhost:8080', + stopSequences: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined> + >({ + name: 'generative-anthropic', + config: { + baseUrl: 'http://localhost:8080', + stopSequences: { values: ['a', 'b', 'c'] }, + }, + }); + }); + }); + + describe('anyscale', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.anyscale(); + expect(config).toEqual< + ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined> + >({ + name: 'generative-anyscale', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.anyscale({ + baseURL: 'http://localhost:8080', + }); + expect(config).toEqual< + ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined> + >({ + name: 'generative-anyscale', + config: { + baseUrl: 'http://localhost:8080', + }, + }); + }); + }); + + describe('aws', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.aws(); + expect(config).toEqual< + ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined> + >({ + name: 'generative-aws', + config: undefined, + }); + }); + }); + + describe('azure-openai', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.azureOpenAI(); + expect(config).toEqual< + ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> + >({ + name: 'generative-azure-openai', + config: { isAzure: true }, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.azureOpenAI({ + baseURL: 'http://localhost:8080', + model: 'model', + stop: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> + >({ + name: 'generative-azure-openai', + config: { + baseUrl: 'http://localhost:8080', + stop: { values: ['a', 'b', 'c'] }, + model: 'model', + isAzure: true, + }, + }); + }); + }); + + describe('cohere', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.cohere(); + expect(config).toEqual< + ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined> + >({ + name: 'generative-cohere', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.cohere({ + baseURL: 'http://localhost:8080', + stopSequences: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined> + >({ + name: 'generative-cohere', + config: { + baseUrl: 'http://localhost:8080', + stopSequences: { values: ['a', 'b', 'c'] }, + }, + }); + }); + }); + + describe('databricks', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.databricks(); + expect(config).toEqual< + ModuleConfig< + 'generative-databricks', + GenerativeConfigRuntimeType<'generative-databricks'> | undefined + > + >({ + name: 'generative-databricks', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.databricks({ + stop: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig< + 'generative-databricks', + GenerativeConfigRuntimeType<'generative-databricks'> | undefined + > + >({ + name: 'generative-databricks', + config: { + stop: { values: ['a', 'b', 'c'] }, + }, + }); + }); + }); + + describe('friendliai', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.friendliai(); + expect(config).toEqual< + ModuleConfig< + 'generative-friendliai', + GenerativeConfigRuntimeType<'generative-friendliai'> | undefined + > + >({ + name: 'generative-friendliai', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.friendliai({ + baseURL: 'http://localhost:8080', + }); + expect(config).toEqual< + ModuleConfig< + 'generative-friendliai', + GenerativeConfigRuntimeType<'generative-friendliai'> | undefined + > + >({ + name: 'generative-friendliai', + config: { + baseUrl: 'http://localhost:8080', + }, + }); + }); + }); + + describe('mistral', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.mistral(); + expect(config).toEqual< + ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined> + >({ + name: 'generative-mistral', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.mistral({ + baseURL: 'http://localhost:8080', + }); + expect(config).toEqual< + ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined> + >({ + name: 'generative-mistral', + config: { + baseUrl: 'http://localhost:8080', + }, + }); + }); + }); + + describe('nvidia', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.nvidia(); + expect(config).toEqual< + ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined> + >({ + name: 'generative-nvidia', + config: undefined, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.nvidia({ + baseURL: 'http://localhost:8080', + }); + expect(config).toEqual< + ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined> + >({ + name: 'generative-nvidia', + config: { + baseUrl: 'http://localhost:8080', + }, + }); + }); + }); + + describe('ollama', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.ollama(); + expect(config).toEqual< + ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined> + >({ + name: 'generative-ollama', + config: undefined, + }); + }); + }); + + describe('openai', () => { + it('with defaults', () => { + const config = generativeConfigRuntime.openAI(); + expect(config).toEqual< + ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> + >({ + name: 'generative-openai', + config: { isAzure: false }, + }); + }); + it('with values', () => { + const config = generativeConfigRuntime.openAI({ + baseURL: 'http://localhost:8080', + model: 'model', + stop: ['a', 'b', 'c'], + }); + expect(config).toEqual< + ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> + >({ + name: 'generative-openai', + config: { + baseUrl: 'http://localhost:8080', + isAzure: false, + model: 'model', + stop: { values: ['a', 'b', 'c'] }, + }, + }); + }); + }); +}); diff --git a/src/collections/query/check.ts b/src/collections/query/check.ts index 291738de..ebe87835 100644 --- a/src/collections/query/check.ts +++ b/src/collections/query/check.ts @@ -98,6 +98,12 @@ export class Check { return check.supports; }; + public supportForSingleGroupedGenerative = async () => { + const check = await this.dbVersionSupport.supportsSingleGrouped(); + if (!check.supports) throw new WeaviateUnsupportedFeatureError(check.message); + return check.supports; + }; + public nearSearch = (opts?: BaseNearOptions) => { return Promise.all([ this.getSearcher(), diff --git a/src/collections/serialize/index.ts b/src/collections/serialize/index.ts index 747bf00a..6ca6585c 100644 --- a/src/collections/serialize/index.ts +++ b/src/collections/serialize/index.ts @@ -25,7 +25,12 @@ import { BatchObject_Properties, BatchObject_SingleTargetRefProps, } from '../../proto/v1/batch.js'; -import { GenerativeSearch } from '../../proto/v1/generative.js'; +import { + GenerativeProvider, + GenerativeSearch, + GenerativeSearch_Grouped, + GenerativeSearch_Single, +} from '../../proto/v1/generative.js'; import { GroupBy, MetadataRequest, @@ -63,6 +68,7 @@ import { SearchNearVectorArgs, SearchNearVideoArgs, } from '../../grpc/searcher.js'; +import { toBase64FromMedia } from '../../index.js'; import { AggregateRequest_Aggregation, AggregateRequest_Aggregation_Boolean, @@ -82,6 +88,7 @@ import { ObjectArrayProperties, ObjectProperties, ObjectPropertiesValue, + TextArray, TextArrayProperties, Vectors as VectorsGrpc, } from '../../proto/v1/base.js'; @@ -97,10 +104,13 @@ import { AggregateBaseOptions, AggregateHybridOptions, AggregateNearOptions, + GenerativeConfigRuntime, GroupByAggregate, + GroupedTask, MultiTargetVectorJoin, PrimitiveKeys, PropertiesMetrics, + SinglePrompt, } from '../index.js'; import { BaseHybridOptions, @@ -818,14 +828,126 @@ export class Serialize { return vec !== undefined && !Array.isArray(vec) && Object.values(vec).some(ArrayInputGuards.is2DArray); }; - public static generative = (generative?: GenerateOptions): GenerativeSearch => { - return GenerativeSearch.fromPartial({ - singleResponsePrompt: generative?.singlePrompt, - groupedResponseTask: generative?.groupedTask, - groupedProperties: generative?.groupedProperties as string[], - }); + private static generativeQuery = async ( + generative: GenerativeConfigRuntime, + opts?: { metadata?: boolean; images?: (string | Buffer)[]; imageProperties?: string[] } + ): Promise => { + const withImages = async >( + config: T, + imgs?: (string | Buffer)[], + imgProps?: string[] + ): Promise => { + if (imgs == undefined && imgProps == undefined) { + return config; + } + return { + ...config, + images: TextArray.fromPartial({ + values: imgs ? await Promise.all(imgs.map(toBase64FromMedia)) : undefined, + }), + imageProperties: TextArray.fromPartial({ values: imgProps }), + }; + }; + + const provider = GenerativeProvider.fromPartial({ returnMetadata: opts?.metadata }); + switch (generative.name) { + case 'generative-anthropic': + provider.anthropic = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + case 'generative-anyscale': + provider.anyscale = generative.config; + break; + case 'generative-aws': + provider.aws = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + case 'generative-cohere': + provider.cohere = generative.config; + break; + case 'generative-databricks': + provider.databricks = generative.config; + break; + case 'generative-dummy': + provider.dummy = generative.config; + break; + case 'generative-friendliai': + provider.friendliai = generative.config; + break; + case 'generative-google': + provider.google = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + case 'generative-mistral': + provider.mistral = generative.config; + break; + case 'generative-nvidia': + provider.nvidia = generative.config; + break; + case 'generative-ollama': + provider.ollama = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + case 'generative-openai': + provider.openai = await withImages(generative.config, opts?.images, opts?.imageProperties); + break; + } + return provider; + }; + + public static generative = async ( + args: { supportsSingleGrouped: boolean }, + opts?: GenerateOptions + ): Promise => { + const singlePrompt = Serialize.isSinglePrompt(opts?.singlePrompt) + ? opts.singlePrompt.prompt + : opts?.singlePrompt; + const singlePromptDebug = Serialize.isSinglePrompt(opts?.singlePrompt) + ? opts.singlePrompt.debug + : undefined; + + const groupedTask = Serialize.isGroupedTask(opts?.groupedTask) + ? opts.groupedTask.prompt + : opts?.groupedTask; + const groupedProperties = Serialize.isGroupedTask(opts?.groupedTask) + ? opts.groupedTask.nonBlobProperties + : opts?.groupedProperties; + + const singleOpts = Serialize.isSinglePrompt(opts?.singlePrompt) ? opts.singlePrompt : undefined; + const groupedOpts = Serialize.isGroupedTask(opts?.groupedTask) ? opts.groupedTask : undefined; + + return args.supportsSingleGrouped + ? GenerativeSearch.fromPartial({ + single: opts?.singlePrompt + ? GenerativeSearch_Single.fromPartial({ + prompt: singlePrompt, + debug: singlePromptDebug, + queries: opts.config ? [await Serialize.generativeQuery(opts.config, singleOpts)] : undefined, + }) + : undefined, + grouped: opts?.groupedTask + ? GenerativeSearch_Grouped.fromPartial({ + task: groupedTask, + queries: opts.config + ? [await Serialize.generativeQuery(opts.config, groupedOpts)] + : undefined, + properties: groupedProperties + ? TextArray.fromPartial({ values: groupedProperties as string[] }) + : undefined, + }) + : undefined, + }) + : GenerativeSearch.fromPartial({ + singleResponsePrompt: singlePrompt, + groupedResponseTask: groupedTask, + groupedProperties: groupedProperties as string[], + }); }; + public static isSinglePrompt(arg?: string | SinglePrompt): arg is SinglePrompt { + return typeof arg !== 'string' && arg !== undefined && arg.prompt !== undefined; + } + + public static isGroupedTask(arg?: string | GroupedTask): arg is GroupedTask { + return typeof arg !== 'string' && arg !== undefined && arg.prompt !== undefined; + } + private static bm25QueryProperties = ( properties?: (PrimitiveKeys | Bm25QueryProperty)[] ): string[] | undefined => { diff --git a/src/collections/serialize/unit.test.ts b/src/collections/serialize/unit.test.ts index 721d1e46..6d9f9612 100644 --- a/src/collections/serialize/unit.test.ts +++ b/src/collections/serialize/unit.test.ts @@ -441,12 +441,15 @@ describe('Unit testing of Serialize', () => { }); }); - it('should parse args for generative', () => { - const args = Serialize.generative({ - singlePrompt: 'test', - groupedProperties: ['name'], - groupedTask: 'testing', - }); + it('should parse args for generative', async () => { + const args = await Serialize.generative( + { supportsSingleGrouped: false }, + { + singlePrompt: 'test', + groupedProperties: ['name'], + groupedTask: 'testing', + } + ); expect(args).toEqual({ singleResponsePrompt: 'test', groupedProperties: ['name'], diff --git a/src/collections/types/generate.ts b/src/collections/types/generate.ts index b3f6bac2..53bf208c 100644 --- a/src/collections/types/generate.ts +++ b/src/collections/types/generate.ts @@ -1,54 +1,314 @@ +import { + GenerativeAWS as GenerativeAWSGRPC, + GenerativeAWSMetadata, + GenerativeAnthropic as GenerativeAnthropicGRPC, + GenerativeAnthropicMetadata, + GenerativeAnyscale as GenerativeAnyscaleGRPC, + GenerativeAnyscaleMetadata, + GenerativeCohere as GenerativeCohereGRPC, + GenerativeCohereMetadata, + GenerativeDatabricks as GenerativeDatabricksGRPC, + GenerativeDatabricksMetadata, + GenerativeDebug, + GenerativeDummy as GenerativeDummyGRPC, + GenerativeDummyMetadata, + GenerativeFriendliAI as GenerativeFriendliAIGRPC, + GenerativeFriendliAIMetadata, + GenerativeGoogle as GenerativeGoogleGRPC, + GenerativeGoogleMetadata, + GenerativeMistral as GenerativeMistralGRPC, + GenerativeMistralMetadata, + GenerativeNvidia as GenerativeNvidiaGRPC, + GenerativeNvidiaMetadata, + GenerativeOllama as GenerativeOllamaGRPC, + GenerativeOllamaMetadata, + GenerativeOpenAI as GenerativeOpenAIGRPC, + GenerativeOpenAIMetadata, +} from '../../proto/v1/generative.js'; +import { ModuleConfig } from '../index.js'; import { GroupByObject, GroupByResult, WeaviateGenericObject, WeaviateNonGenericObject } from './query.js'; -export type GenerativeGenericObject = WeaviateGenericObject & { - /** The LLM-generated output applicable to this single object. */ +export type GenerativeGenericObject< + T, + C extends GenerativeConfigRuntime | undefined +> = WeaviateGenericObject & { + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this single object. */ generated?: string; + /** Generative data returned from the LLM inference on this object. */ + generative?: GenerativeSingle; }; -export type GenerativeNonGenericObject = WeaviateNonGenericObject & { - /** The LLM-generated output applicable to this single object. */ - generated?: string; -}; +export type GenerativeNonGenericObject = + WeaviateNonGenericObject & { + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this single object. */ + generated?: string; + /** Generative data returned from the LLM inference on this object. */ + generative?: GenerativeSingle; + }; /** An object belonging to a collection as returned by the methods in the `collection.generate` namespace. * * Depending on the generic type `T`, the object will have subfields that map from `T`'s specific type definition. * If not, then the object will be non-generic and have a `properties` field that maps from a generic string to a `WeaviateField`. */ -export type GenerativeObject = T extends undefined - ? GenerativeNonGenericObject - : GenerativeGenericObject; +export type GenerativeObject = T extends undefined + ? GenerativeNonGenericObject + : GenerativeGenericObject; + +export type GenerativeSingle = { + debug?: GenerativeDebug; + metadata?: GenerativeMetadata; + text?: string; +}; + +export type GenerativeGrouped = { + metadata?: GenerativeMetadata; + text?: string; +}; /** The return of a query method in the `collection.generate` namespace. */ -export type GenerativeReturn = { +export type GenerativeReturn = { /** The objects that were found by the query. */ - objects: GenerativeObject[]; - /** The LLM-generated output applicable to this query as a whole. */ + objects: GenerativeObject[]; + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this query as a whole. */ generated?: string; + generative?: GenerativeGrouped; }; -export type GenerativeGroupByResult = GroupByResult & { +export type GenerativeGroupByResult = GroupByResult & { + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this query as a whole. */ generated?: string; + generative?: GenerativeSingle; }; /** The return of a query method in the `collection.generate` namespace where the `groupBy` argument was specified. */ -export type GenerativeGroupByReturn = { +export type GenerativeGroupByReturn = { /** The objects that were found by the query. */ objects: GroupByObject[]; /** The groups that were created by the query. */ - groups: Record>; - /** The LLM-generated output applicable to this query as a whole. */ + groups: Record>; + /** @deprecated (use `generative.text` instead) The LLM-generated output applicable to this query as a whole. */ generated?: string; + generative?: GenerativeGrouped; }; /** Options available when defining queries using methods in the `collection.generate` namespace. */ -export type GenerateOptions = { +export type GenerateOptions = { /** The prompt to use when generating content relevant to each object of the collection individually. */ - singlePrompt?: string; + singlePrompt?: string | SinglePrompt; /** The prompt to use when generating content relevant to objects returned by the query as a whole. */ - groupedTask?: string; + groupedTask?: string | GroupedTask; /** The properties to use as context to be injected into the `groupedTask` prompt when performing the grouped generation. */ groupedProperties?: T extends undefined ? string[] : (keyof T)[]; + config?: C; +}; + +export type SinglePrompt = { + prompt: string; + debug?: boolean; + metadata?: boolean; + images?: (string | Buffer)[]; + imageProperties?: string[]; +}; + +export type GroupedTask = { + prompt: string; + metadata?: boolean; + nonBlobProperties?: T extends undefined ? string[] : (keyof T)[]; + images?: (string | Buffer)[]; + imageProperties?: string[]; +}; + +type omitFields = 'images' | 'imageProperties'; + +export type GenerativeConfigRuntime = + | ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'>> + | ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'>> + | ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'>> + | ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> + | ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'>> + | ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-databricks'>> + | ModuleConfig<'generative-dummy', GenerativeConfigRuntimeType<'generative-dummy'>> + | ModuleConfig<'generative-friendliai', GenerativeConfigRuntimeType<'generative-friendliai'>> + | ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-google'>> + | ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'>> + | ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'>> + | ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'>> + | ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>>; + +export type GenerativeConfigRuntimeType = G extends 'generative-anthropic' + ? Omit + : G extends 'generative-anyscale' + ? Omit + : G extends 'generative-aws' + ? Omit + : G extends 'generative-azure-openai' + ? Omit & { isAzure: true } + : G extends 'generative-cohere' + ? Omit + : G extends 'generative-databricks' + ? Omit + : G extends 'generative-google' + ? Omit + : G extends 'generative-friendliai' + ? Omit + : G extends 'generative-mistral' + ? Omit + : G extends 'generative-nvidia' + ? Omit + : G extends 'generative-ollama' + ? Omit + : G extends 'generative-openai' + ? Omit & { isAzure?: false } + : G extends 'none' + ? undefined + : Record | undefined; + +export type GenerativeMetadata = C extends undefined + ? never + : C extends infer R extends GenerativeConfigRuntime + ? R['name'] extends 'generative-anthropic' + ? GenerativeAnthropicMetadata + : R['name'] extends 'generative-anyscale' + ? GenerativeAnyscaleMetadata + : R['name'] extends 'generative-aws' + ? GenerativeAWSMetadata + : R['name'] extends 'generative-cohere' + ? GenerativeCohereMetadata + : R['name'] extends 'generative-databricks' + ? GenerativeDatabricksMetadata + : R['name'] extends 'generative-dummy' + ? GenerativeDummyMetadata + : R['name'] extends 'generative-friendliai' + ? GenerativeFriendliAIMetadata + : R['name'] extends 'generative-google' + ? GenerativeGoogleMetadata + : R['name'] extends 'generative-mistral' + ? GenerativeMistralMetadata + : R['name'] extends 'generative-nvidia' + ? GenerativeNvidiaMetadata + : R['name'] extends 'generative-ollama' + ? GenerativeOllamaMetadata + : R['name'] extends 'generative-openai' + ? GenerativeOpenAIMetadata + : never + : never; + +export type GenerateReturn = + | Promise> + | Promise>; + +export type GenerativeAnthropicConfigRuntime = { + baseURL?: string | undefined; + maxTokens?: number | undefined; + model?: string | undefined; + temperature?: number | undefined; + topK?: number | undefined; + topP?: number | undefined; + stopSequences?: string[] | undefined; +}; + +export type GenerativeAnyscaleConfigRuntime = { + baseURL?: string | undefined; + model?: string | undefined; + temperature?: number | undefined; }; -export type GenerateReturn = Promise> | Promise>; +export type GenerativeAWSConfigRuntime = { + model?: string | undefined; + temperature?: number | undefined; + service?: string | undefined; + region?: string | undefined; + endpoint?: string | undefined; + targetModel?: string | undefined; + targetVariant?: string | undefined; +}; + +export type GenerativeCohereConfigRuntime = { + baseURL?: string | undefined; + frequencyPenalty?: number | undefined; + maxTokens?: number | undefined; + model?: string | undefined; + k?: number | undefined; + p?: number | undefined; + presencePenalty?: number | undefined; + stopSequences?: string[] | undefined; + temperature?: number | undefined; +}; + +export type GenerativeDatabricksConfigRuntime = { + endpoint?: string | undefined; + model?: string | undefined; + frequencyPenalty?: number | undefined; + logProbs?: boolean | undefined; + topLogProbs?: number | undefined; + maxTokens?: number | undefined; + n?: number | undefined; + presencePenalty?: number | undefined; + stop?: string[] | undefined; + temperature?: number | undefined; + topP?: number | undefined; +}; + +export type GenerativeDummyConfigRuntime = GenerativeDummyGRPC; + +export type GenerativeFriendliAIConfigRuntime = { + baseURL?: string | undefined; + model?: string | undefined; + maxTokens?: number | undefined; + temperature?: number | undefined; + n?: number | undefined; + topP?: number | undefined; +}; + +export type GenerativeGoogleConfigRuntime = { + frequencyPenalty?: number | undefined; + maxTokens?: number | undefined; + model?: string | undefined; + presencePenalty?: number | undefined; + temperature?: number | undefined; + topK?: number | undefined; + topP?: number | undefined; + stopSequences?: string[] | undefined; + apiEndpoint?: string | undefined; + projectId?: string | undefined; + endpointId?: string | undefined; + region?: string | undefined; +}; + +export type GenerativeMistralConfigRuntime = { + baseURL?: string | undefined; + maxTokens?: number | undefined; + model?: string | undefined; + temperature?: number | undefined; + topP?: number | undefined; +}; + +export type GenerativeNvidiaConfigRuntime = { + baseURL?: string | undefined; + model?: string | undefined; + temperature?: number | undefined; + topP?: number | undefined; + maxTokens?: number | undefined; +}; + +export type GenerativeOllamaConfigRuntime = { + apiEndpoint?: string | undefined; + model?: string | undefined; + temperature?: number | undefined; +}; + +export type GenerativeOpenAIConfigRuntime = { + frequencyPenalty?: number | undefined; + maxTokens?: number | undefined; + model?: string; + n?: number | undefined; + presencePenalty?: number | undefined; + stop?: string[] | undefined; + temperature?: number | undefined; + topP?: number | undefined; + baseURL?: string | undefined; + apiVersion?: string | undefined; + resourceName?: string | undefined; + deploymentId?: string | undefined; +}; diff --git a/src/grpc/searcher.ts b/src/grpc/searcher.ts index 6dc6103e..20496c6f 100644 --- a/src/grpc/searcher.ts +++ b/src/grpc/searcher.ts @@ -171,6 +171,7 @@ export default class Searcher extends Base implements Search { tenant: this.tenant, uses123Api: true, uses125Api: true, + uses127Api: true, }, { metadata: this.metadata, diff --git a/src/proto/v1/generative.ts b/src/proto/v1/generative.ts index 2abae4ba..fe0805fa 100644 --- a/src/proto/v1/generative.ts +++ b/src/proto/v1/generative.ts @@ -118,7 +118,7 @@ export interface GenerativeOllama { export interface GenerativeOpenAI { frequencyPenalty?: number | undefined; maxTokens?: number | undefined; - model: string; + model?: string | undefined; n?: number | undefined; presencePenalty?: number | undefined; stop?: TextArray | undefined; @@ -1884,7 +1884,7 @@ function createBaseGenerativeOpenAI(): GenerativeOpenAI { return { frequencyPenalty: undefined, maxTokens: undefined, - model: "", + model: undefined, n: undefined, presencePenalty: undefined, stop: undefined, @@ -1908,7 +1908,7 @@ export const GenerativeOpenAI = { if (message.maxTokens !== undefined) { writer.uint32(16).int64(message.maxTokens); } - if (message.model !== "") { + if (message.model !== undefined) { writer.uint32(26).string(message.model); } if (message.n !== undefined) { @@ -2075,7 +2075,7 @@ export const GenerativeOpenAI = { return { frequencyPenalty: isSet(object.frequencyPenalty) ? globalThis.Number(object.frequencyPenalty) : undefined, maxTokens: isSet(object.maxTokens) ? globalThis.Number(object.maxTokens) : undefined, - model: isSet(object.model) ? globalThis.String(object.model) : "", + model: isSet(object.model) ? globalThis.String(object.model) : undefined, n: isSet(object.n) ? globalThis.Number(object.n) : undefined, presencePenalty: isSet(object.presencePenalty) ? globalThis.Number(object.presencePenalty) : undefined, stop: isSet(object.stop) ? TextArray.fromJSON(object.stop) : undefined, @@ -2099,7 +2099,7 @@ export const GenerativeOpenAI = { if (message.maxTokens !== undefined) { obj.maxTokens = Math.round(message.maxTokens); } - if (message.model !== "") { + if (message.model !== undefined) { obj.model = message.model; } if (message.n !== undefined) { @@ -2148,7 +2148,7 @@ export const GenerativeOpenAI = { const message = createBaseGenerativeOpenAI(); message.frequencyPenalty = object.frequencyPenalty ?? undefined; message.maxTokens = object.maxTokens ?? undefined; - message.model = object.model ?? ""; + message.model = object.model ?? undefined; message.n = object.n ?? undefined; message.presencePenalty = object.presencePenalty ?? undefined; message.stop = (object.stop !== undefined && object.stop !== null) ? TextArray.fromPartial(object.stop) : undefined; diff --git a/src/utils/dbVersion.ts b/src/utils/dbVersion.ts index 279537e2..707b5f26 100644 --- a/src/utils/dbVersion.ts +++ b/src/utils/dbVersion.ts @@ -219,6 +219,17 @@ export class DbVersionSupport { }; }); }; + + supportsSingleGrouped = () => + this.dbVersionProvider.getVersion().then((version) => ({ + version, + supports: + (version.isAtLeast(1, 27, 14) && version.isLowerThan(1, 28, 0)) || + (version.isAtLeast(1, 28, 8) && version.isLowerThan(1, 29, 0)) || + (version.isAtLeast(1, 29, 0) && version.isLowerThan(1, 30, 0)) || + version.isAtLeast(1, 30, 0), + message: this.errorMessage('Single/Grouped fields in gRPC', version.show(), '1.30.0'), + })); } const EMPTY_VERSION = ''; From 87501e0857d81cbfa78636380406795ceb0a3854 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 17 Mar 2025 14:16:31 +0000 Subject: [PATCH 14/49] Add ability to define tenant-level perms for `data` and `tenants` perms --- src/roles/index.ts | 114 +++++++- src/roles/integration.test.ts | 492 +++++++++++++++++++--------------- src/roles/types.ts | 2 + src/roles/util.ts | 29 +- 4 files changed, 406 insertions(+), 231 deletions(-) diff --git a/src/roles/index.ts b/src/roles/index.ts index 2fbe723b..cef48b68 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -121,6 +121,15 @@ const roles = (connection: ConnectionREST): Roles => { }; export const permissions = { + /** + * Create a set of permissions specific to Weaviate's backup functionality. + * + * For all collections, provide the `collection` argument as `'*'`. + * + * @param {string | string[]} args.collection The collection or collections to create permissions for. + * @param {boolean} [args.manage] Whether to allow managing backups. Defaults to `false`. + * @returns {BackupsPermission[]} The permissions for the specified collections. + */ backup: (args: { collection: string | string[]; manage?: boolean }): BackupsPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; return collections.flatMap((collection) => { @@ -129,11 +138,27 @@ export const permissions = { return out; }); }, + /** + * Create a set of permissions specific to Weaviate's cluster endpoints. + * + * @param {boolean} [args.read] Whether to allow reading cluster information. Defaults to `false`. + */ cluster: (args: { read?: boolean }): ClusterPermission[] => { const out: ClusterPermission = { actions: [] }; if (args.read) out.actions.push('read_cluster'); return [out]; }, + /** + * Create a set of permissions specific to any operations involving collections. + * + * For all collections, provide the `collection` argument as `'*'`. + * + * @param {string | string[]} args.collection The collection or collections to create permissions for. + * @param {boolean} [args.create_collection] Whether to allow creating collections. Defaults to `false`. + * @param {boolean} [args.read_config] Whether to allow reading collection configurations. Defaults to `false`. + * @param {boolean} [args.update_config] Whether to allow updating collection configurations. Defaults to `false`. + * @param {boolean} [args.delete_collection] Whether to allow deleting collections. Defaults to `false`. + */ collections: (args: { collection: string | string[]; create_collection?: boolean; @@ -151,16 +176,37 @@ export const permissions = { return out; }); }, + /** + * Create a set of permissions specific to any operations involving objects within collections and tenants. + * + * For all collections, provide the `collection` argument as `'*'`. + * For all tenants, provide the `tenant` argument as `'*'`. + * + * Providing arrays of collections and tenants will create permissions for each combination of collection and tenant. + * E.g., `data({ collection: ['A', 'B'], tenant: ['X', 'Y'] })` will create permissions for tenants `X` and `Y` in both collections `A` and `B`. + * + * @param {string | string[]} args.collection The collection or collections to create permissions for. + * @param {string | string[]} [args.tenant] The tenant or tenants to create permissions for. Defaults to `'*'`. + * @param {boolean} [args.create] Whether to allow creating objects. Defaults to `false`. + * @param {boolean} [args.read] Whether to allow reading objects. Defaults to `false`. + * @param {boolean} [args.update] Whether to allow updating objects. Defaults to `false`. + * @param {boolean} [args.delete] Whether to allow deleting objects. Defaults to `false`. + */ data: (args: { collection: string | string[]; + tenant?: string | string[]; create?: boolean; read?: boolean; update?: boolean; delete?: boolean; }): DataPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; - return collections.flatMap((collection) => { - const out: DataPermission = { collection, actions: [] }; + const tenants = Array.isArray(args.tenant) ? args.tenant : [args.tenant ?? '*']; + const combinations = collections.flatMap((collection) => + tenants.map((tenant) => ({ collection, tenant })) + ); + return combinations.flatMap(({ collection, tenant }) => { + const out: DataPermission = { collection, tenant, actions: [] }; if (args.create) out.actions.push('create_data'); if (args.read) out.actions.push('read_data'); if (args.update) out.actions.push('update_data'); @@ -168,7 +214,16 @@ export const permissions = { return out; }); }, + /** + * This namespace contains methods to create permissions specific to nodes. + */ nodes: { + /** + * Create a set of permissions specific to reading nodes with verbosity set to `minimal`. + * + * @param {boolean} [args.read] Whether to allow reading nodes. Defaults to `false`. + * @returns {NodesPermission[]} The permissions for reading nodes. + */ minimal: (args: { read?: boolean }): NodesPermission[] => { const out: NodesPermission = { collection: '*', @@ -178,6 +233,13 @@ export const permissions = { if (args.read) out.actions.push('read_nodes'); return [out]; }, + /** + * Create a set of permissions specific to reading nodes with verbosity set to `verbose`. + * + * @param {string | string[]} args.collection The collection or collections to create permissions for. + * @param {boolean} [args.read] Whether to allow reading nodes. Defaults to `false`. + * @returns {NodesPermission[]} The permissions for reading nodes. + */ verbose: (args: { collection: string | string[]; read?: boolean }): NodesPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; return collections.flatMap((collection) => { @@ -191,6 +253,16 @@ export const permissions = { }); }, }, + /** + * Create a set of permissions specific to any operations involving roles. + * + * @param {string | string[]} args.role The role or roles to create permissions for. + * @param {boolean} [args.create] Whether to allow creating roles. Defaults to `false`. + * @param {boolean} [args.read] Whether to allow reading roles. Defaults to `false`. + * @param {boolean} [args.update] Whether to allow updating roles. Defaults to `false`. + * @param {boolean} [args.delete] Whether to allow deleting roles. Defaults to `false`. + * @returns {RolesPermission[]} The permissions for the specified roles. + */ roles: (args: { role: string | string[]; create?: boolean; @@ -208,16 +280,38 @@ export const permissions = { return out; }); }, + /** + * Create a set of permissions specific to any operations involving tenants. + * + * For all collections, provide the `collection` argument as `'*'`. + * For all tenants, provide the `tenant` argument as `'*'`. + * + * Providing arrays of collections and tenants will create permissions for each combination of collection and tenant. + * E.g., `tenants({ collection: ['A', 'B'], tenant: ['X', 'Y'] })` will create permissions for tenants `X` and `Y` in both collections `A` and `B`. + * + * @param {string | string[] | Record} args.collection The collection or collections to create permissions for. + * @param {string | string[]} [args.tenant] The tenant or tenants to create permissions for. Defaults to `'*'`. + * @param {boolean} [args.create] Whether to allow creating tenants. Defaults to `false`. + * @param {boolean} [args.read] Whether to allow reading tenants. Defaults to `false`. + * @param {boolean} [args.update] Whether to allow updating tenants. Defaults to `false`. + * @param {boolean} [args.delete] Whether to allow deleting tenants. Defaults to `false`. + * @returns {TenantsPermission[]} The permissions for the specified tenants. + */ tenants: (args: { collection: string | string[]; + tenant?: string | string[]; create?: boolean; read?: boolean; update?: boolean; delete?: boolean; }): TenantsPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; - return collections.flatMap((collection) => { - const out: TenantsPermission = { collection, actions: [] }; + const tenants = Array.isArray(args.tenant) ? args.tenant : [args.tenant ?? '*']; + const combinations = collections.flatMap((collection) => + tenants.map((tenant) => ({ collection, tenant })) + ); + return combinations.flatMap(({ collection, tenant }) => { + const out: TenantsPermission = { collection, tenant, actions: [] }; if (args.create) out.actions.push('create_tenants'); if (args.read) out.actions.push('read_tenants'); if (args.update) out.actions.push('update_tenants'); @@ -225,15 +319,23 @@ export const permissions = { return out; }); }, + /** + * Create a set of permissions specific to any operations involving users. + * + * @param {string | string[]} args.user The user or users to create permissions for. + * @param {boolean} [args.assignAndRevoke] Whether to allow assigning and revoking users. Defaults to `false`. + * @param {boolean} [args.read] Whether to allow reading users. Defaults to `false`. + * @returns {UsersPermission[]} The permissions for the specified users. + */ users: (args: { user: string | string[]; - assign_and_revoke?: boolean; + assignAndRevoke?: boolean; read?: boolean; }): UsersPermission[] => { const users = Array.isArray(args.user) ? args.user : [args.user]; return users.flatMap((user) => { const out: UsersPermission = { users: user, actions: [] }; - if (args.assign_and_revoke) out.actions.push('assign_and_revoke_users'); + if (args.assignAndRevoke) out.actions.push('assign_and_revoke_users'); if (args.read) out.actions.push('read_users'); return out; }); diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index 7d083209..2963276f 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -1,12 +1,288 @@ -import weaviate, { ApiKey, Permission, Role, WeaviateClient } from '..'; +import weaviate, { + ApiKey, + CollectionsAction, + DataAction, + Permission, + Role, + RolesAction, + TenantsAction, + WeaviateClient, +} from '..'; import { WeaviateStartUpError, WeaviateUnexpectedStatusCodeError } from '../errors'; import { DbVersion } from '../utils/dbVersion'; -const only = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0) +type TestCase = { + roleName: string; + permissions: Permission[]; + expected: Role; +}; + +const emptyPermissions = { + backupsPermissions: [], + clusterPermissions: [], + collectionsPermissions: [], + dataPermissions: [], + nodesPermissions: [], + rolesPermissions: [], + tenantsPermissions: [], + usersPermissions: [], +}; +const crud = { + create: true, + read: true, + update: true, + delete: true, +}; +const collectionsActions: CollectionsAction[] = [ + 'create_collections', + 'read_collections', + 'update_collections', + 'delete_collections', +]; +const dataActions: DataAction[] = ['create_data', 'read_data', 'update_data', 'delete_data']; +const tenantsActions: TenantsAction[] = [ + 'create_tenants', + 'read_tenants', + 'update_tenants', + 'delete_tenants', +]; +const rolesActions: RolesAction[] = ['create_roles', 'read_roles', 'update_roles', 'delete_roles']; +const testCases: TestCase[] = [ + { + roleName: 'backups', + permissions: weaviate.permissions.backup({ collection: 'Some-collection', manage: true }), + expected: { + name: 'backups', + ...emptyPermissions, + backupsPermissions: [{ collection: 'Some-collection', actions: ['manage_backups'] }], + }, + }, + { + roleName: 'cluster', + permissions: weaviate.permissions.cluster({ read: true }), + expected: { + name: 'cluster', + ...emptyPermissions, + clusterPermissions: [{ actions: ['read_cluster'] }], + }, + }, + { + roleName: 'collections', + permissions: weaviate.permissions.collections({ + collection: 'Some-collection', + create_collection: true, + read_config: true, + update_config: true, + delete_collection: true, + }), + expected: { + name: 'collections', + ...emptyPermissions, + collectionsPermissions: [ + { + collection: 'Some-collection', + actions: collectionsActions, + }, + ], + }, + }, + { + roleName: 'data-st', + permissions: weaviate.permissions.data({ + collection: 'Some-collection', + ...crud, + }), + expected: { + name: 'data-st', + ...emptyPermissions, + dataPermissions: [ + { + collection: 'Some-collection', + tenant: '*', + actions: dataActions, + }, + ], + }, + }, + { + roleName: 'data-mt', + permissions: weaviate.permissions.data({ + collection: 'Some-collection', + tenant: 'some-tenant', + ...crud, + }), + expected: { + name: 'data-mt', + ...emptyPermissions, + dataPermissions: [ + { + collection: 'Some-collection', + tenant: 'some-tenant', + actions: dataActions, + }, + ], + }, + }, + { + roleName: 'data-mt-mixed', + permissions: weaviate.permissions.data({ + collection: ['Some-collection', 'Another-collection'], + tenant: ['some-tenant', 'another-tenant'], + ...crud, + }), + expected: { + name: 'data-mt-mixed', + ...emptyPermissions, + dataPermissions: [ + { + collection: 'Some-collection', + tenant: 'some-tenant', + actions: dataActions, + }, + { + collection: 'Some-collection', + tenant: 'another-tenant', + actions: dataActions, + }, + { + collection: 'Another-collection', + tenant: 'some-tenant', + actions: dataActions, + }, + { + collection: 'Another-collection', + tenant: 'another-tenant', + actions: dataActions, + }, + ], + }, + }, + { + roleName: 'nodes-verbose', + permissions: weaviate.permissions.nodes.verbose({ + collection: 'Some-collection', + read: true, + }), + expected: { + name: 'nodes-verbose', + ...emptyPermissions, + nodesPermissions: [{ collection: 'Some-collection', verbosity: 'verbose', actions: ['read_nodes'] }], + }, + }, + { + roleName: 'nodes-minimal', + permissions: weaviate.permissions.nodes.minimal({ + read: true, + }), + expected: { + name: 'nodes-minimal', + ...emptyPermissions, + nodesPermissions: [{ collection: '*', verbosity: 'minimal', actions: ['read_nodes'] }], + }, + }, + { + roleName: 'roles', + permissions: weaviate.permissions.roles({ + role: 'some-role', + ...crud, + }), + expected: { + name: 'roles', + ...emptyPermissions, + rolesPermissions: [{ role: 'some-role', actions: rolesActions }], + }, + }, + { + roleName: 'tenants-st', + permissions: weaviate.permissions.tenants({ + collection: 'some-collection', + ...crud, + }), + expected: { + name: 'tenants-st', + ...emptyPermissions, + tenantsPermissions: [ + { + collection: 'Some-collection', + tenant: '*', + actions: tenantsActions, + }, + ], + }, + }, + { + roleName: 'tenants-mt', + permissions: weaviate.permissions.tenants({ + collection: 'some-collection', + tenant: 'some-tenant', + ...crud, + }), + expected: { + name: 'tenants-mt', + ...emptyPermissions, + tenantsPermissions: [ + { + collection: 'Some-collection', + tenant: 'some-tenant', + actions: tenantsActions, + }, + ], + }, + }, + { + roleName: 'tenants-mt-mixed', + permissions: weaviate.permissions.tenants({ + collection: ['some-collection', 'another-collection'], + tenant: ['some-tenant', 'another-tenant'], + ...crud, + }), + expected: { + name: 'tenants-mt-mixed', + ...emptyPermissions, + tenantsPermissions: [ + { + collection: 'Some-collection', + tenant: 'some-tenant', + actions: tenantsActions, + }, + { + collection: 'Some-collection', + tenant: 'another-tenant', + actions: tenantsActions, + }, + { + collection: 'Another-collection', + tenant: 'some-tenant', + actions: tenantsActions, + }, + { + collection: 'Another-collection', + tenant: 'another-tenant', + actions: tenantsActions, + }, + ], + }, + }, + { + roleName: 'users', + permissions: weaviate.permissions.users({ + user: 'some-user', + assignAndRevoke: true, + read: true, + }), + expected: { + name: 'users', + ...emptyPermissions, + usersPermissions: [{ users: 'some-user', actions: ['assign_and_revoke_users', 'read_users'] }], + }, + }, +]; + +const maybe = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0) ? describe : describe.skip; -only('Integration testing of the roles namespace', () => { +maybe('Integration testing of the roles namespace', () => { let client: WeaviateClient; beforeAll(async () => { @@ -41,200 +317,6 @@ only('Integration testing of the roles namespace', () => { }); describe('should be able to create roles using the permissions factory', () => { - type TestCase = { - roleName: string; - permissions: Permission[]; - expected: Role; - }; - const testCases: TestCase[] = [ - { - roleName: 'backups', - permissions: weaviate.permissions.backup({ collection: 'Some-collection', manage: true }), - expected: { - name: 'backups', - backupsPermissions: [{ collection: 'Some-collection', actions: ['manage_backups'] }], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'cluster', - permissions: weaviate.permissions.cluster({ read: true }), - expected: { - name: 'cluster', - backupsPermissions: [], - clusterPermissions: [{ actions: ['read_cluster'] }], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'collections', - permissions: weaviate.permissions.collections({ - collection: 'Some-collection', - create_collection: true, - read_config: true, - update_config: true, - delete_collection: true, - }), - expected: { - name: 'collections', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [ - { - collection: 'Some-collection', - actions: ['create_collections', 'read_collections', 'update_collections', 'delete_collections'], - }, - ], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'data', - permissions: weaviate.permissions.data({ - collection: 'Some-collection', - create: true, - read: true, - update: true, - delete: true, - }), - expected: { - name: 'data', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [ - { - collection: 'Some-collection', - actions: ['create_data', 'read_data', 'update_data', 'delete_data'], - }, - ], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'nodes-verbose', - permissions: weaviate.permissions.nodes.verbose({ - collection: 'Some-collection', - read: true, - }), - expected: { - name: 'nodes-verbose', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [ - { collection: 'Some-collection', verbosity: 'verbose', actions: ['read_nodes'] }, - ], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'nodes-minimal', - permissions: weaviate.permissions.nodes.minimal({ - read: true, - }), - expected: { - name: 'nodes-minimal', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [{ collection: '*', verbosity: 'minimal', actions: ['read_nodes'] }], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'roles', - permissions: weaviate.permissions.roles({ - role: 'some-role', - create: true, - read: true, - update: true, - delete: true, - }), - expected: { - name: 'roles', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [ - { role: 'some-role', actions: ['create_roles', 'read_roles', 'update_roles', 'delete_roles'] }, - ], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'tenants', - permissions: weaviate.permissions.tenants({ - collection: 'some-collection', - create: true, - read: true, - update: true, - delete: true, - }), - expected: { - name: 'tenants', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [ - { - collection: 'Some-collection', - actions: ['create_tenants', 'read_tenants', 'update_tenants', 'delete_tenants'], - }, - ], - usersPermissions: [], - }, - }, - { - roleName: 'users', - permissions: weaviate.permissions.users({ - user: 'some-user', - assign_and_revoke: true, - read: true, - }), - expected: { - name: 'users', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [{ users: 'some-user', actions: ['assign_and_revoke_users', 'read_users'] }], - }, - }, - ]; testCases.forEach((testCase) => { it(`with ${testCase.roleName} permissions`, async () => { await client.roles.create(testCase.roleName, testCase.permissions); @@ -250,19 +332,5 @@ only('Integration testing of the roles namespace', () => { await expect(client.roles.exists('backups')).resolves.toBeFalsy(); }); - afterAll(() => - Promise.all( - [ - 'backups', - 'cluster', - 'collections', - 'data', - 'nodes-verbose', - 'nodes-minimal', - 'roles', - 'tenants', - 'users', - ].map((n) => client.roles.delete(n)) - ) - ); + afterAll(() => Promise.all(testCases.map((t) => client.roles.delete(t.roleName)))); }); diff --git a/src/roles/types.ts b/src/roles/types.ts index 6f314245..3a6d0266 100644 --- a/src/roles/types.ts +++ b/src/roles/types.ts @@ -38,6 +38,7 @@ export type CollectionsPermission = { export type DataPermission = { collection: string; + tenant: string; actions: DataAction[]; }; @@ -54,6 +55,7 @@ export type RolesPermission = { export type TenantsPermission = { collection: string; + tenant: string; actions: TenantsAction[]; }; diff --git a/src/roles/util.ts b/src/roles/util.ts index b4a8a9bf..df3072ef 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -81,35 +81,35 @@ export class Map { static permissionToWeaviate = (permission: Permission): WeaviatePermission[] => { if (PermissionGuards.isBackups(permission)) { return Array.from(permission.actions).map((action) => ({ - backups: { collection: permission.collection }, + backups: permission, action, })); } else if (PermissionGuards.isCluster(permission)) { return Array.from(permission.actions).map((action) => ({ action })); } else if (PermissionGuards.isCollections(permission)) { return Array.from(permission.actions).map((action) => ({ - collections: { collection: permission.collection }, + collections: permission, action, })); } else if (PermissionGuards.isData(permission)) { return Array.from(permission.actions).map((action) => ({ - data: { collection: permission.collection }, + data: permission, action, })); } else if (PermissionGuards.isNodes(permission)) { return Array.from(permission.actions).map((action) => ({ - nodes: { collection: permission.collection, verbosity: permission.verbosity }, + nodes: permission, action, })); } else if (PermissionGuards.isRoles(permission)) { - return Array.from(permission.actions).map((action) => ({ roles: { role: permission.role }, action })); + return Array.from(permission.actions).map((action) => ({ roles: permission, action })); } else if (PermissionGuards.isTenants(permission)) { return Array.from(permission.actions).map((action) => ({ - tenants: { collection: permission.collection }, + tenants: permission, action, })); } else if (PermissionGuards.isUsers(permission)) { - return Array.from(permission.actions).map((action) => ({ users: { users: permission.users }, action })); + return Array.from(permission.actions).map((action) => ({ users: permission, action })); } else { throw new Error(`Unknown permission type: ${JSON.stringify(permission, null, 2)}`); } @@ -198,9 +198,11 @@ class PermissionsMapping { private data = (permission: WeaviatePermission) => { if (permission.data !== undefined) { - const key = permission.data.collection; - if (key === undefined) throw new Error('Data permission missing collection'); - if (this.mappings.data[key] === undefined) this.mappings.data[key] = { collection: key, actions: [] }; + const { collection, tenant } = permission.data; + if (collection === undefined) throw new Error('Data permission missing collection'); + const key = tenant === undefined ? collection : `${collection}#${tenant}`; + if (this.mappings.data[key] === undefined) + this.mappings.data[key] = { collection, tenant: tenant || '*', actions: [] }; this.mappings.data[key].actions.push(permission.action as DataAction); } }; @@ -232,10 +234,11 @@ class PermissionsMapping { private tenants = (permission: WeaviatePermission) => { if (permission.tenants !== undefined) { - const key = permission.tenants.collection; - if (key === undefined) throw new Error('Tenants permission missing collection'); + const { collection, tenant } = permission.tenants; + if (collection === undefined) throw new Error('Tenants permission missing collection'); + const key = tenant === undefined ? collection : `${collection}#${tenant}`; if (this.mappings.tenants[key] === undefined) - this.mappings.tenants[key] = { collection: key, actions: [] }; + this.mappings.tenants[key] = { collection, tenant: tenant || '*', actions: [] }; this.mappings.tenants[key].actions.push(permission.action as TenantsAction); } }; From 525cc2742711695fa7c9a811d67f3387cf2229ff Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Tue, 18 Mar 2025 13:05:46 +0000 Subject: [PATCH 15/49] Make the next publish job push a next tag (add bash script to do this automatically in future) --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1d5b1a0b..14d970ac 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -125,7 +125,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build - - run: npm publish + - run: npm publish --tag next env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} - run: npm run docs From 2fcb05f4d250a8d6fd585b4cf4db47e4a9fb2243 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Tue, 18 Mar 2025 13:06:18 +0000 Subject: [PATCH 16/49] 3.5.0-beta.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c8df7cc..dc095958 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "weaviate-client", - "version": "3.4.1", + "version": "3.5.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "weaviate-client", - "version": "3.4.1", + "version": "3.5.0-beta.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { "abort-controller-x": "^0.4.3", diff --git a/package.json b/package.json index bda5dbd3..83a4d9a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weaviate-client", - "version": "3.4.1", + "version": "3.5.0-beta.0", "description": "JS/TS client for Weaviate", "main": "dist/node/cjs/index.js", "type": "module", From 69ae2c4c94439a24e5c5f5d39f8bbe5bf4e0b8e4 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Wed, 19 Mar 2025 12:50:54 +0000 Subject: [PATCH 17/49] Fix action typo, add generic to catch this in future --- src/roles/util.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/roles/util.ts b/src/roles/util.ts index df3072ef..bee179db 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -3,6 +3,7 @@ import { User } from '../users/types.js'; import { BackupsAction, BackupsPermission, + ClusterAction, ClusterPermission, CollectionsAction, CollectionsPermission, @@ -22,36 +23,40 @@ import { } from './types.js'; export class PermissionGuards { - private static includes = (permission: Permission, ...actions: string[]): boolean => + private static includes = (permission: Permission, ...actions: A[]): boolean => actions.filter((a) => Array.from(permission.actions).includes(a)).length > 0; static isBackups = (permission: Permission): permission is BackupsPermission => - PermissionGuards.includes(permission, 'manage_backups'); + PermissionGuards.includes(permission, 'manage_backups'); static isCluster = (permission: Permission): permission is ClusterPermission => - PermissionGuards.includes(permission, 'read_cluster'); + PermissionGuards.includes(permission, 'read_cluster'); static isCollections = (permission: Permission): permission is CollectionsPermission => - PermissionGuards.includes( + PermissionGuards.includes( permission, 'create_collections', 'delete_collections', 'read_collections', - 'update_collections', - 'manage_collections' + 'update_collections' ); static isData = (permission: Permission): permission is DataPermission => - PermissionGuards.includes( + PermissionGuards.includes( permission, 'create_data', 'delete_data', 'read_data', - 'update_data', - 'manage_data' + 'update_data' ); static isNodes = (permission: Permission): permission is NodesPermission => - PermissionGuards.includes(permission, 'read_nodes'); + PermissionGuards.includes(permission, 'read_nodes'); static isRoles = (permission: Permission): permission is RolesPermission => - PermissionGuards.includes(permission, 'create_role', 'read_roles', 'update_roles', 'delete_roles'); + PermissionGuards.includes( + permission, + 'create_roles', + 'read_roles', + 'update_roles', + 'delete_roles' + ); static isTenants = (permission: Permission): permission is TenantsPermission => - PermissionGuards.includes( + PermissionGuards.includes( permission, 'create_tenants', 'delete_tenants', @@ -59,7 +64,7 @@ export class PermissionGuards { 'update_tenants' ); static isUsers = (permission: Permission): permission is UsersPermission => - PermissionGuards.includes(permission, 'read_users', 'assign_and_revoke_users'); + PermissionGuards.includes(permission, 'read_users', 'assign_and_revoke_users'); static isPermission = (permissions: PermissionsInput): permissions is Permission => !Array.isArray(permissions); static isPermissionArray = (permissions: PermissionsInput): permissions is Permission[] => From ccf14ed59cdc31fb3a4aa0f9f5924023572a071d Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Wed, 19 Mar 2025 12:51:27 +0000 Subject: [PATCH 18/49] 3.5.0-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc095958..ce7b4d3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "weaviate-client", - "version": "3.5.0-beta.0", + "version": "3.5.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "weaviate-client", - "version": "3.5.0-beta.0", + "version": "3.5.0-beta.1", "license": "SEE LICENSE IN LICENSE", "dependencies": { "abort-controller-x": "^0.4.3", diff --git a/package.json b/package.json index 83a4d9a8..45521f41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weaviate-client", - "version": "3.5.0-beta.0", + "version": "3.5.0-beta.1", "description": "JS/TS client for Weaviate", "main": "dist/node/cjs/index.js", "type": "module", From 3eb52edbc6c8379384c79c3fcca6958d0f57c8cb Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Fri, 21 Mar 2025 14:28:57 +0000 Subject: [PATCH 19/49] Fix broken add/remove permissions method --- src/roles/index.ts | 8 ++++++-- src/roles/integration.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/roles/index.ts b/src/roles/index.ts index cef48b68..89163d38 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -106,9 +106,13 @@ const roles = (connection: ConnectionREST): Roles => { .then(() => true) .catch(() => false), addPermissions: (roleName: string, permissions: PermissionsInput) => - connection.postEmpty(`/authz/roles/${roleName}/add-permissions`, { permissions }), + connection.postEmpty(`/authz/roles/${roleName}/add-permissions`, { + permissions: Map.flattenPermissions(permissions).flatMap(Map.permissionToWeaviate), + }), removePermissions: (roleName: string, permissions: PermissionsInput) => - connection.postEmpty(`/authz/roles/${roleName}/remove-permissions`, { permissions }), + connection.postEmpty(`/authz/roles/${roleName}/remove-permissions`, { + permissions: Map.flattenPermissions(permissions).flatMap(Map.permissionToWeaviate), + }), hasPermissions: (roleName: string, permission: Permission | Permission[]) => Promise.all( (Array.isArray(permission) ? permission : [permission]) diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index 2963276f..f90f0465 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -326,6 +326,35 @@ maybe('Integration testing of the roles namespace', () => { }); }); + it('should be able to add permissions to one of the created roles', async () => { + await client.roles.addPermissions( + 'backups', + weaviate.permissions.backup({ collection: 'Another-collection', manage: true }) + ); + const role = await client.roles.byName('backups'); + expect(role).toEqual({ + name: 'backups', + ...emptyPermissions, + backupsPermissions: [ + { collection: 'Some-collection', actions: ['manage_backups'] }, + { collection: 'Another-collection', actions: ['manage_backups'] }, + ], + }); + }); + + it('should be able to remove permissions from one of the created roles', async () => { + await client.roles.removePermissions( + 'backups', + weaviate.permissions.backup({ collection: 'Another-collection', manage: true }) + ); + const role = await client.roles.byName('backups'); + expect(role).toEqual({ + name: 'backups', + ...emptyPermissions, + backupsPermissions: [{ collection: 'Some-collection', actions: ['manage_backups'] }], + }); + }); + it('should delete one of the created roles', async () => { await client.roles.delete('backups'); await expect(client.roles.byName('backups')).rejects.toThrowError(WeaviateUnexpectedStatusCodeError); From 25d0f8109b75e09f5e82c34a59a788557fb79527 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Fri, 21 Mar 2025 14:29:11 +0000 Subject: [PATCH 20/49] 3.5.0-beta.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce7b4d3c..ef3e1a6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "weaviate-client", - "version": "3.5.0-beta.1", + "version": "3.5.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "weaviate-client", - "version": "3.5.0-beta.1", + "version": "3.5.0-beta.2", "license": "SEE LICENSE IN LICENSE", "dependencies": { "abort-controller-x": "^0.4.3", diff --git a/package.json b/package.json index 45521f41..ef02c92a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weaviate-client", - "version": "3.5.0-beta.1", + "version": "3.5.0-beta.2", "description": "JS/TS client for Weaviate", "main": "dist/node/cjs/index.js", "type": "module", From 43bbb92f387bc88787900290c4a60e1f7d76191b Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Mon, 31 Mar 2025 12:13:49 +0100 Subject: [PATCH 21/49] Make fixes in response to user feedback --- .github/workflows/main.yaml | 2 +- src/collections/deserialize/index.ts | 8 ++++ src/collections/generate/index.ts | 48 ++++++++++++++++---- src/collections/generate/integration.test.ts | 26 +++++++++++ src/collections/query/check.ts | 9 +++- src/roles/index.ts | 10 ++-- src/utils/dbVersion.ts | 7 +++ 7 files changed, 96 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 14d970ac..6baf3620 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -13,7 +13,7 @@ env: WEAVIATE_127: 1.27.15 WEAVIATE_128: 1.28.11 WEAVIATE_129: 1.29.1 - WEAVIATE_130: 1.30.0-dev-9df35ca + WEAVIATE_130: 1.30.0-rc.0-6b9a01c concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/src/collections/deserialize/index.ts b/src/collections/deserialize/index.ts index 41f9beff..fdf37f3d 100644 --- a/src/collections/deserialize/index.ts +++ b/src/collections/deserialize/index.ts @@ -226,6 +226,10 @@ export class Deserialize { debug: result.generative.values[0].debug, metadata: result.generative.values[0].metadata as GenerativeMetadata, } + : result.metadata?.generativePresent + ? { + text: result.metadata?.generative, + } : undefined, metadata: Deserialize.metadata(result.metadata), properties: this.properties(result.properties), @@ -245,6 +249,10 @@ export class Deserialize { text: reply.generativeGroupedResults?.values[0].result, metadata: reply.generativeGroupedResults?.values[0].metadata as GenerativeMetadata, } + : reply.generativeGroupedResult !== '' + ? { + text: reply.generativeGroupedResult, + } : undefined, }; } diff --git a/src/collections/generate/index.ts b/src/collections/generate/index.ts index 799313f8..bf010fd9 100644 --- a/src/collections/generate/index.ts +++ b/src/collections/generate/index.ts @@ -72,7 +72,11 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: FetchObjectsOptions ): Promise> { - return Promise.all([this.check.fetchObjects(opts), this.check.supportForSingleGroupedGenerative()]) + return Promise.all([ + this.check.fetchObjects(opts), + this.check.supportForSingleGroupedGenerative(), + this.check.supportForGenerativeConfigRuntime(generate.config), + ]) .then(async ([{ search }, supportsSingleGrouped]) => search.withFetch({ ...Serialize.search.fetchObjects(opts), @@ -97,7 +101,11 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: Bm25Options ): GenerateReturn { - return Promise.all([this.check.bm25(opts), this.check.supportForSingleGroupedGenerative()]) + return Promise.all([ + this.check.bm25(opts), + this.check.supportForSingleGroupedGenerative(), + this.check.supportForGenerativeConfigRuntime(generate.config), + ]) .then(async ([{ search }, supportsSingleGrouped]) => search.withBm25({ ...Serialize.search.bm25(query, opts), @@ -122,7 +130,11 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: HybridOptions ): GenerateReturn { - return Promise.all([this.check.hybridSearch(opts), this.check.supportForSingleGroupedGenerative()]) + return Promise.all([ + this.check.hybridSearch(opts), + this.check.supportForSingleGroupedGenerative(), + this.check.supportForGenerativeConfigRuntime(generate.config), + ]) .then( async ([ { search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }, @@ -159,7 +171,11 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) + return Promise.all([ + this.check.nearSearch(opts), + this.check.supportForSingleGroupedGenerative(), + this.check.supportForGenerativeConfigRuntime(generate.config), + ]) .then(([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => Promise.all([ toBase64FromMedia(image), @@ -196,7 +212,11 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) + return Promise.all([ + this.check.nearSearch(opts), + this.check.supportForSingleGroupedGenerative(), + this.check.supportForGenerativeConfigRuntime(generate.config), + ]) .then(async ([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => search.withNearObject({ ...Serialize.search.nearObject( @@ -228,7 +248,11 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) + return Promise.all([ + this.check.nearSearch(opts), + this.check.supportForSingleGroupedGenerative(), + this.check.supportForGenerativeConfigRuntime(generate.config), + ]) .then(async ([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => search.withNearText({ ...Serialize.search.nearText( @@ -260,7 +284,11 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearVector(vector, opts), this.check.supportForSingleGroupedGenerative()]) + return Promise.all([ + this.check.nearVector(vector, opts), + this.check.supportForSingleGroupedGenerative(), + this.check.supportForGenerativeConfigRuntime(generate.config), + ]) .then( async ([ { search, supportsTargets, supportsVectorsForTargets, supportsWeightsForTargets }, @@ -300,7 +328,11 @@ class GenerateManager implements Generate { generate: GenerateOptions, opts?: NearOptions ): GenerateReturn { - return Promise.all([this.check.nearSearch(opts), this.check.supportForSingleGroupedGenerative()]) + return Promise.all([ + this.check.nearSearch(opts), + this.check.supportForSingleGroupedGenerative(), + this.check.supportForGenerativeConfigRuntime(generate.config), + ]) .then(([{ search, supportsTargets, supportsWeightsForTargets }, supportsSingleGrouped]) => { const args = { supportsTargets, diff --git a/src/collections/generate/integration.test.ts b/src/collections/generate/integration.test.ts index 6e941d96..50a7bed9 100644 --- a/src/collections/generate/integration.test.ts +++ b/src/collections/generate/integration.test.ts @@ -148,6 +148,26 @@ maybe('Testing of the collection.generate methods with a simple collection', () expect(ret.objects[0].uuid).toEqual(id); expect(ret.objects[0].generated).toBeDefined(); }); + + it('should generate in a BC-compatible way', async () => { + const query = () => collection.generate.fetchObjects(generateOpts); + + const res = await query(); + expect(res.objects.length).toEqual(1); + expect(res.generated).toBeDefined(); + expect(res.generated).not.toEqual(''); + expect(res.generative?.text).toBeDefined(); + expect(res.generative?.text).not.toEqual(''); + expect(res.generative?.metadata).toBeUndefined(); + res.objects.forEach((obj) => { + expect(obj.generated).toBeDefined(); + expect(obj.generated).not.toEqual(''); + expect(obj.generative?.text).toBeDefined(); + expect(obj.generative?.text).not.toEqual(''); + expect(obj.generative?.metadata).toBeUndefined(); + expect(obj.generative?.debug).toBeUndefined(); + }); + }); }); }); @@ -486,7 +506,9 @@ maybe('Testing of the collection.generate methods with runtime generative config const res = await query(); expect(res.objects.length).toEqual(1); expect(res.generated).toBeDefined(); + expect(res.generated).not.toEqual(''); expect(res.generative?.text).toBeDefined(); + expect(res.generative?.text).not.toEqual(''); expect(res.generative?.metadata).toBeDefined(); res.objects.forEach((obj) => { expect(obj.generated).toBeDefined(); @@ -518,11 +540,15 @@ maybe('Testing of the collection.generate methods with runtime generative config const res = await query(); expect(res.objects.length).toEqual(1); expect(res.generated).toBeDefined(); + expect(res.generated).not.toEqual(''); expect(res.generative?.text).toBeDefined(); + expect(res.generative?.text).not.toEqual(''); expect(res.generative?.metadata).toBeUndefined(); res.objects.forEach((obj) => { expect(obj.generated).toBeDefined(); + expect(obj.generated).not.toEqual(''); expect(obj.generative?.text).toBeDefined(); + expect(obj.generative?.text).not.toEqual(''); expect(obj.generative?.metadata).toBeUndefined(); expect(obj.generative?.debug).toBeUndefined(); }); diff --git a/src/collections/query/check.ts b/src/collections/query/check.ts index ebe87835..2e562a2b 100644 --- a/src/collections/query/check.ts +++ b/src/collections/query/check.ts @@ -2,7 +2,7 @@ import Connection from '../../connection/grpc.js'; import { WeaviateUnsupportedFeatureError } from '../../errors.js'; import { ConsistencyLevel } from '../../index.js'; import { DbVersionSupport } from '../../utils/dbVersion.js'; -import { GroupByOptions } from '../index.js'; +import { GenerativeConfigRuntime, GroupByOptions } from '../index.js'; import { Serialize } from '../serialize/index.js'; import { BaseBm25Options, @@ -104,6 +104,13 @@ export class Check { return check.supports; }; + public supportForGenerativeConfigRuntime = async (generativeConfig?: GenerativeConfigRuntime) => { + if (generativeConfig === undefined) return true; + const check = await this.dbVersionSupport.supportsGenerativeConfigRuntime(); + if (!check.supports) throw new WeaviateUnsupportedFeatureError(check.message); + return check.supports; + }; + public nearSearch = (opts?: BaseNearOptions) => { return Promise.all([ this.getSearcher(), diff --git a/src/roles/index.ts b/src/roles/index.ts index 89163d38..e5dd42fa 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -90,14 +90,16 @@ const roles = (connection: ConnectionREST): Roles => { byName: (roleName: string) => connection.get(`/authz/roles/${roleName}`).then(Map.roleFromWeaviate), assignedUserIds: (roleName: string) => connection.get(`/authz/roles/${roleName}/users`), - create: (roleName: string, permissions: PermissionsInput) => { - const perms = Map.flattenPermissions(permissions).flatMap(Map.permissionToWeaviate); + create: (roleName: string, permissions?: PermissionsInput) => { + const perms = permissions + ? Map.flattenPermissions(permissions).flatMap(Map.permissionToWeaviate) + : undefined; return connection - .postEmpty('/authz/roles', { + .postEmpty('/authz/roles', { name: roleName, permissions: perms, }) - .then(() => Map.roleFromWeaviate({ name: roleName, permissions: perms })); + .then(() => Map.roleFromWeaviate({ name: roleName, permissions: perms || [] })); }, delete: (roleName: string) => connection.delete(`/authz/roles/${roleName}`, null), exists: (roleName: string) => diff --git a/src/utils/dbVersion.ts b/src/utils/dbVersion.ts index 707b5f26..d5265095 100644 --- a/src/utils/dbVersion.ts +++ b/src/utils/dbVersion.ts @@ -230,6 +230,13 @@ export class DbVersionSupport { version.isAtLeast(1, 30, 0), message: this.errorMessage('Single/Grouped fields in gRPC', version.show(), '1.30.0'), })); + + supportsGenerativeConfigRuntime = () => + this.dbVersionProvider.getVersion().then((version) => ({ + version, + supports: version.isAtLeast(1, 30, 0), + message: this.errorMessage('Generative config runtime', version.show(), '1.30.0'), + })); } const EMPTY_VERSION = ''; From 184f8d6199ab59a2c04f08da18a258b483fd31e3 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Tue, 1 Apr 2025 12:42:00 +0100 Subject: [PATCH 22/49] Improvements: (#279) - Fixes bad typing of `GenerativeConfigRuntime` - adds mocks for this usage - renames `generativeConfigRuntime` to `generativeParameters` to match Python naming --- src/collections/generate/config.ts | 2 +- src/collections/generate/index.ts | 2 +- src/collections/generate/integration.test.ts | 5 +- src/collections/generate/mock.test.ts | 169 +++++++++++++++++++ src/collections/generate/unit.test.ts | 44 ++--- src/collections/serialize/index.ts | 24 +-- src/collections/types/generate.ts | 22 +-- 7 files changed, 219 insertions(+), 49 deletions(-) create mode 100644 src/collections/generate/mock.test.ts diff --git a/src/collections/generate/config.ts b/src/collections/generate/config.ts index 1ab12b04..2f5eb714 100644 --- a/src/collections/generate/config.ts +++ b/src/collections/generate/config.ts @@ -15,7 +15,7 @@ import { GenerativeOpenAIConfigRuntime, } from '../index.js'; -export const generativeConfigRuntime = { +export const generativeParameters = { /** * Create a `ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined>` object for use when performing runtime-specific AI generation using the `generative-anthropic` module. * diff --git a/src/collections/generate/index.ts b/src/collections/generate/index.ts index bf010fd9..3a5fb1b3 100644 --- a/src/collections/generate/index.ts +++ b/src/collections/generate/index.ts @@ -396,5 +396,5 @@ class GenerateManager implements Generate { export default GenerateManager.use; -export { generativeConfigRuntime } from './config.js'; +export { generativeParameters } from './config.js'; export { Generate } from './types.js'; diff --git a/src/collections/generate/integration.test.ts b/src/collections/generate/integration.test.ts index 50a7bed9..39e8e351 100644 --- a/src/collections/generate/integration.test.ts +++ b/src/collections/generate/integration.test.ts @@ -1,9 +1,10 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ import { WeaviateUnsupportedFeatureError } from '../../errors.js'; -import weaviate, { WeaviateClient, generativeConfigRuntime } from '../../index.js'; +import weaviate, { WeaviateClient } from '../../index.js'; import { Collection } from '../collection/index.js'; import { GenerateOptions, GroupByOptions } from '../types/index.js'; +import { generativeParameters } from './config.js'; const maybe = process.env.OPENAI_APIKEY ? describe : describe.skip; @@ -493,7 +494,7 @@ maybe('Testing of the collection.generate methods with runtime generative config nonBlobProperties: ['testProp'], metadata: true, }, - config: generativeConfigRuntime.openAI({ + config: generativeParameters.openAI({ stop: ['\n'], }), }); diff --git a/src/collections/generate/mock.test.ts b/src/collections/generate/mock.test.ts new file mode 100644 index 00000000..d4a0c025 --- /dev/null +++ b/src/collections/generate/mock.test.ts @@ -0,0 +1,169 @@ +import express from 'express'; +import { Server as HttpServer } from 'http'; +import { Server as GrpcServer, createServer } from 'nice-grpc'; +import weaviate, { Collection, GenerativeConfigRuntime, WeaviateClient } from '../..'; +import { + HealthCheckRequest, + HealthCheckResponse, + HealthCheckResponse_ServingStatus, + HealthDefinition, + HealthServiceImplementation, +} from '../../proto/google/health/v1/health'; +import { GenerativeResult } from '../../proto/v1/generative'; +import { SearchReply, SearchRequest, SearchResult } from '../../proto/v1/search_get'; +import { WeaviateDefinition, WeaviateServiceImplementation } from '../../proto/v1/weaviate'; +import { generativeParameters } from './config'; + +const mockedSingleGenerative = 'Mocked single response'; +const mockedGroupedGenerative = 'Mocked group response'; + +class GenerateMock { + private grpc: GrpcServer; + private http: HttpServer; + + constructor(grpc: GrpcServer, http: HttpServer) { + this.grpc = grpc; + this.http = http; + } + + public static use = async (version: string, httpPort: number, grpcPort: number) => { + const httpApp = express(); + // Meta endpoint required for client instantiation + httpApp.get('/v1/meta', (req, res) => res.send({ version })); + + // gRPC health check required for client instantiation + const healthMockImpl: HealthServiceImplementation = { + check: (request: HealthCheckRequest): Promise => + Promise.resolve(HealthCheckResponse.create({ status: HealthCheckResponse_ServingStatus.SERVING })), + watch: jest.fn(), + }; + + const grpc = createServer(); + grpc.add(HealthDefinition, healthMockImpl); + + // Search endpoint returning generative mock data + const weaviateMockImpl: WeaviateServiceImplementation = { + aggregate: jest.fn(), + tenantsGet: jest.fn(), + search: (req: SearchRequest): Promise => { + expect(req.generative?.grouped?.queries.length).toBeGreaterThan(0); + expect(req.generative?.single?.queries.length).toBeGreaterThan(0); + return Promise.resolve( + SearchReply.fromPartial({ + results: [ + SearchResult.fromPartial({ + properties: { + nonRefProps: { fields: { name: { textValue: 'thing' } } }, + }, + generative: GenerativeResult.fromPartial({ + values: [ + { + result: mockedSingleGenerative, + }, + ], + }), + metadata: { + id: 'b602a271-d5a9-4324-921d-5abe4748d6b5', + }, + }), + ], + generativeGroupedResults: GenerativeResult.fromPartial({ + values: [ + { + result: mockedGroupedGenerative, + }, + ], + }), + }) + ); + }, + batchDelete: jest.fn(), + batchObjects: jest.fn(), + }; + grpc.add(WeaviateDefinition, weaviateMockImpl); + + await grpc.listen(`localhost:${grpcPort}`); + const http = await httpApp.listen(httpPort); + return new GenerateMock(grpc, http); + }; + + public close = () => Promise.all([this.http.close(), this.grpc.shutdown()]); +} + +describe('Mock testing of generate with runtime config', () => { + let client: WeaviateClient; + let collection: Collection; + let mock: GenerateMock; + + beforeAll(async () => { + mock = await GenerateMock.use('1.30.0-rc.1', 8958, 8959); + client = await weaviate.connectToLocal({ port: 8958, grpcPort: 8959 }); + collection = client.collections.use('Whatever'); + }); + + afterAll(() => mock.close()); + + const stringTest = (config: GenerativeConfigRuntime) => + collection.generate + .fetchObjects({ + singlePrompt: 'What is the meaning of life?', + groupedTask: 'What is the meaning of life?', + config: config, + }) + .then((res) => { + expect(res.generative?.text).toEqual(mockedGroupedGenerative); + expect(res.objects[0].generative?.text).toEqual(mockedSingleGenerative); + }); + + const objectTest = (config: GenerativeConfigRuntime) => + collection.generate + .fetchObjects({ + singlePrompt: { + prompt: 'What is the meaning of life?', + }, + groupedTask: { + prompt: 'What is the meaning of life?', + }, + config: config, + }) + .then((res) => { + expect(res.generative?.text).toEqual(mockedGroupedGenerative); + expect(res.objects[0].generative?.text).toEqual(mockedSingleGenerative); + }); + + const model = { model: 'llama-2' }; + + const tests: GenerativeConfigRuntime[] = [ + generativeParameters.anthropic(), + generativeParameters.anthropic(model), + generativeParameters.anyscale(), + generativeParameters.anyscale(model), + generativeParameters.aws(), + generativeParameters.aws(model), + generativeParameters.azureOpenAI(), + generativeParameters.azureOpenAI(model), + generativeParameters.cohere(), + generativeParameters.cohere(model), + generativeParameters.databricks(), + generativeParameters.databricks(model), + generativeParameters.friendliai(), + generativeParameters.friendliai(model), + generativeParameters.google(), + generativeParameters.google(model), + generativeParameters.mistral(), + generativeParameters.mistral(model), + generativeParameters.nvidia(), + generativeParameters.nvidia(model), + generativeParameters.ollama(), + generativeParameters.ollama(model), + generativeParameters.openAI(), + generativeParameters.openAI(model), + ]; + + tests.forEach((conf) => { + it(`should get the mocked response for ${conf.name} with config: ${conf.config}`, async () => { + await stringTest(conf); + await objectTest(conf); + }); + }); +}); diff --git a/src/collections/generate/unit.test.ts b/src/collections/generate/unit.test.ts index 63ff17b9..54ecb335 100644 --- a/src/collections/generate/unit.test.ts +++ b/src/collections/generate/unit.test.ts @@ -1,11 +1,11 @@ import { GenerativeConfigRuntimeType, ModuleConfig } from '../types'; -import { generativeConfigRuntime } from './config'; +import { generativeParameters } from './config'; // only tests fields that must be mapped from some public name to a gRPC name, e.g. baseURL -> baseUrl and stop: string[] -> stop: TextArray -describe('Unit testing of the generativeConfigRuntime factory methods', () => { +describe('Unit testing of the generativeParameters factory methods', () => { describe('anthropic', () => { it('with defaults', () => { - const config = generativeConfigRuntime.anthropic(); + const config = generativeParameters.anthropic(); expect(config).toEqual< ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined> >({ @@ -14,7 +14,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.anthropic({ + const config = generativeParameters.anthropic({ baseURL: 'http://localhost:8080', stopSequences: ['a', 'b', 'c'], }); @@ -32,7 +32,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('anyscale', () => { it('with defaults', () => { - const config = generativeConfigRuntime.anyscale(); + const config = generativeParameters.anyscale(); expect(config).toEqual< ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined> >({ @@ -41,7 +41,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.anyscale({ + const config = generativeParameters.anyscale({ baseURL: 'http://localhost:8080', }); expect(config).toEqual< @@ -57,7 +57,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('aws', () => { it('with defaults', () => { - const config = generativeConfigRuntime.aws(); + const config = generativeParameters.aws(); expect(config).toEqual< ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined> >({ @@ -69,7 +69,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('azure-openai', () => { it('with defaults', () => { - const config = generativeConfigRuntime.azureOpenAI(); + const config = generativeParameters.azureOpenAI(); expect(config).toEqual< ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> >({ @@ -78,7 +78,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.azureOpenAI({ + const config = generativeParameters.azureOpenAI({ baseURL: 'http://localhost:8080', model: 'model', stop: ['a', 'b', 'c'], @@ -99,7 +99,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('cohere', () => { it('with defaults', () => { - const config = generativeConfigRuntime.cohere(); + const config = generativeParameters.cohere(); expect(config).toEqual< ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined> >({ @@ -108,7 +108,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.cohere({ + const config = generativeParameters.cohere({ baseURL: 'http://localhost:8080', stopSequences: ['a', 'b', 'c'], }); @@ -126,7 +126,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('databricks', () => { it('with defaults', () => { - const config = generativeConfigRuntime.databricks(); + const config = generativeParameters.databricks(); expect(config).toEqual< ModuleConfig< 'generative-databricks', @@ -138,7 +138,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.databricks({ + const config = generativeParameters.databricks({ stop: ['a', 'b', 'c'], }); expect(config).toEqual< @@ -157,7 +157,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('friendliai', () => { it('with defaults', () => { - const config = generativeConfigRuntime.friendliai(); + const config = generativeParameters.friendliai(); expect(config).toEqual< ModuleConfig< 'generative-friendliai', @@ -169,7 +169,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.friendliai({ + const config = generativeParameters.friendliai({ baseURL: 'http://localhost:8080', }); expect(config).toEqual< @@ -188,7 +188,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('mistral', () => { it('with defaults', () => { - const config = generativeConfigRuntime.mistral(); + const config = generativeParameters.mistral(); expect(config).toEqual< ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined> >({ @@ -197,7 +197,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.mistral({ + const config = generativeParameters.mistral({ baseURL: 'http://localhost:8080', }); expect(config).toEqual< @@ -213,7 +213,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('nvidia', () => { it('with defaults', () => { - const config = generativeConfigRuntime.nvidia(); + const config = generativeParameters.nvidia(); expect(config).toEqual< ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined> >({ @@ -222,7 +222,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.nvidia({ + const config = generativeParameters.nvidia({ baseURL: 'http://localhost:8080', }); expect(config).toEqual< @@ -238,7 +238,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('ollama', () => { it('with defaults', () => { - const config = generativeConfigRuntime.ollama(); + const config = generativeParameters.ollama(); expect(config).toEqual< ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined> >({ @@ -250,7 +250,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { describe('openai', () => { it('with defaults', () => { - const config = generativeConfigRuntime.openAI(); + const config = generativeParameters.openAI(); expect(config).toEqual< ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>> >({ @@ -259,7 +259,7 @@ describe('Unit testing of the generativeConfigRuntime factory methods', () => { }); }); it('with values', () => { - const config = generativeConfigRuntime.openAI({ + const config = generativeParameters.openAI({ baseURL: 'http://localhost:8080', model: 'model', stop: ['a', 'b', 'c'], diff --git a/src/collections/serialize/index.ts b/src/collections/serialize/index.ts index 3bb06477..e9a74973 100644 --- a/src/collections/serialize/index.ts +++ b/src/collections/serialize/index.ts @@ -852,40 +852,40 @@ export class Serialize { const provider = GenerativeProvider.fromPartial({ returnMetadata: opts?.metadata }); switch (generative.name) { case 'generative-anthropic': - provider.anthropic = await withImages(generative.config, opts?.images, opts?.imageProperties); + provider.anthropic = await withImages(generative.config || {}, opts?.images, opts?.imageProperties); break; case 'generative-anyscale': - provider.anyscale = generative.config; + provider.anyscale = generative.config || {}; break; case 'generative-aws': - provider.aws = await withImages(generative.config, opts?.images, opts?.imageProperties); + provider.aws = await withImages(generative.config || {}, opts?.images, opts?.imageProperties); break; case 'generative-cohere': - provider.cohere = generative.config; + provider.cohere = generative.config || {}; break; case 'generative-databricks': - provider.databricks = generative.config; + provider.databricks = generative.config || {}; break; case 'generative-dummy': - provider.dummy = generative.config; + provider.dummy = generative.config || {}; break; case 'generative-friendliai': - provider.friendliai = generative.config; + provider.friendliai = generative.config || {}; break; case 'generative-google': - provider.google = await withImages(generative.config, opts?.images, opts?.imageProperties); + provider.google = await withImages(generative.config || {}, opts?.images, opts?.imageProperties); break; case 'generative-mistral': - provider.mistral = generative.config; + provider.mistral = generative.config || {}; break; case 'generative-nvidia': - provider.nvidia = generative.config; + provider.nvidia = generative.config || {}; break; case 'generative-ollama': - provider.ollama = await withImages(generative.config, opts?.images, opts?.imageProperties); + provider.ollama = await withImages(generative.config || {}, opts?.images, opts?.imageProperties); break; case 'generative-openai': - provider.openai = await withImages(generative.config, opts?.images, opts?.imageProperties); + provider.openai = await withImages(generative.config || {}, opts?.images, opts?.imageProperties); break; } return provider; diff --git a/src/collections/types/generate.ts b/src/collections/types/generate.ts index 53bf208c..edd16e71 100644 --- a/src/collections/types/generate.ts +++ b/src/collections/types/generate.ts @@ -122,18 +122,18 @@ export type GroupedTask = { type omitFields = 'images' | 'imageProperties'; export type GenerativeConfigRuntime = - | ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'>> - | ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'>> - | ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'>> + | ModuleConfig<'generative-anthropic', GenerativeConfigRuntimeType<'generative-anthropic'> | undefined> + | ModuleConfig<'generative-anyscale', GenerativeConfigRuntimeType<'generative-anyscale'> | undefined> + | ModuleConfig<'generative-aws', GenerativeConfigRuntimeType<'generative-aws'> | undefined> | ModuleConfig<'generative-azure-openai', GenerativeConfigRuntimeType<'generative-azure-openai'>> - | ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'>> - | ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-databricks'>> - | ModuleConfig<'generative-dummy', GenerativeConfigRuntimeType<'generative-dummy'>> - | ModuleConfig<'generative-friendliai', GenerativeConfigRuntimeType<'generative-friendliai'>> - | ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-google'>> - | ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'>> - | ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'>> - | ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'>> + | ModuleConfig<'generative-cohere', GenerativeConfigRuntimeType<'generative-cohere'> | undefined> + | ModuleConfig<'generative-databricks', GenerativeConfigRuntimeType<'generative-databricks'> | undefined> + | ModuleConfig<'generative-dummy', GenerativeConfigRuntimeType<'generative-dummy'> | undefined> + | ModuleConfig<'generative-friendliai', GenerativeConfigRuntimeType<'generative-friendliai'> | undefined> + | ModuleConfig<'generative-google', GenerativeConfigRuntimeType<'generative-google'> | undefined> + | ModuleConfig<'generative-mistral', GenerativeConfigRuntimeType<'generative-mistral'> | undefined> + | ModuleConfig<'generative-nvidia', GenerativeConfigRuntimeType<'generative-nvidia'> | undefined> + | ModuleConfig<'generative-ollama', GenerativeConfigRuntimeType<'generative-ollama'> | undefined> | ModuleConfig<'generative-openai', GenerativeConfigRuntimeType<'generative-openai'>>; export type GenerativeConfigRuntimeType = G extends 'generative-anthropic' From 0abc234936a9a3a18734f41c507cb7fe8cf3a796 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Tue, 1 Apr 2025 13:45:45 +0100 Subject: [PATCH 23/49] Use REST instead of gRPC for `tenants.getByName` when available due to filtering issues with rbac --- src/collections/tenants/index.ts | 27 ++++++++++++++------- src/collections/tenants/integration.test.ts | 21 +++------------- src/utils/dbVersion.ts | 9 ++++++- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/collections/tenants/index.ts b/src/collections/tenants/index.ts index f72c6959..320f166d 100644 --- a/src/collections/tenants/index.ts +++ b/src/collections/tenants/index.ts @@ -1,5 +1,5 @@ import { ConnectionGRPC } from '../../connection/index.js'; -import { WeaviateUnsupportedFeatureError } from '../../errors.js'; +import { WeaviateUnexpectedStatusCodeError, WeaviateUnsupportedFeatureError } from '../../errors.js'; import { Tenant as TenantREST } from '../../openapi/types.js'; import { TenantsCreator, TenantsDeleter, TenantsGetter, TenantsUpdater } from '../../schema/index.js'; import { DbVersionSupport } from '../../utils/dbVersion.js'; @@ -17,12 +17,10 @@ const parseValueOrValueArray = (value: V | V[]) => (Array.isArray(value) ? va const parseStringOrTenant = (tenant: string | T) => typeof tenant === 'string' ? tenant : tenant.name; -const parseTenantREST = (tenant: TenantREST): Tenant => { - return { - name: tenant.name!, - activityStatus: Deserialize.activityStatusREST(tenant.activityStatus), - }; -}; +const parseTenantREST = (tenant: TenantREST): Tenant => ({ + name: tenant.name!, + activityStatus: Deserialize.activityStatusREST(tenant.activityStatus), +}); const tenants = ( connection: ConnectionGRPC, @@ -53,9 +51,20 @@ const tenants = ( return check.supports ? getGRPC() : getREST(); }, getByNames: (tenants: (string | T)[]) => getGRPC(tenants.map(parseStringOrTenant)), - getByName: (tenant: string | T) => { + getByName: async (tenant: string | T) => { const tenantName = parseStringOrTenant(tenant); - return getGRPC([tenantName]).then((tenants) => tenants[tenantName] || null); + if (await dbVersionSupport.supportsTenantGetRESTMethod().then((check) => !check.supports)) { + return getGRPC([tenantName]).then((tenants) => tenants[tenantName] ?? null); + } + return connection + .get(`/schema/${collection}/tenants/${tenantName}`) + .then(parseTenantREST) + .catch((err) => { + if (err instanceof WeaviateUnexpectedStatusCodeError && err.code === 404) { + return null; + } + throw err; + }); }, remove: (tenants: string | T | (string | T)[]) => new TenantsDeleter( diff --git a/src/collections/tenants/integration.test.ts b/src/collections/tenants/integration.test.ts index b574759a..af3c6c95 100644 --- a/src/collections/tenants/integration.test.ts +++ b/src/collections/tenants/integration.test.ts @@ -84,34 +84,19 @@ describe('Testing of the collection.tenants methods', () => { describe('getByName and getByNames', () => { it('should be able to get a tenant by name string', async () => { - const query = () => collection.tenants.getByName('hot'); - if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 25, 0))) { - await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); - return; - } - const result = await query(); + const result = await collection.tenants.getByName('hot'); expect(result).toHaveProperty('name', 'hot'); expect(result).toHaveProperty('activityStatus', 'ACTIVE'); }); it('should be able to get a tenant by tenant object', async () => { - const query = () => collection.tenants.getByName({ name: 'hot' }); - if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 25, 0))) { - await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); - return; - } - const result = await query(); + const result = await collection.tenants.getByName({ name: 'hot' }); expect(result).toHaveProperty('name', 'hot'); expect(result).toHaveProperty('activityStatus', 'ACTIVE'); }); it('should fail to get a non-existing tenant', async () => { - const query = () => collection.tenants.getByName('non-existing'); - if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 25, 0))) { - await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); - return; - } - const result = await query(); + const result = await collection.tenants.getByName('non-existing'); expect(result).toBeNull(); }); diff --git a/src/utils/dbVersion.ts b/src/utils/dbVersion.ts index d5265095..a945e414 100644 --- a/src/utils/dbVersion.ts +++ b/src/utils/dbVersion.ts @@ -151,11 +151,18 @@ export class DbVersionSupport { return { version: version, supports: version.isAtLeast(1, 25, 0), - message: this.errorMessage('Tenants get method', version.show(), '1.25.0'), + message: this.errorMessage('Tenants get method over gRPC', version.show(), '1.25.0'), }; }); }; + supportsTenantGetRESTMethod = () => + this.dbVersionProvider.getVersion().then((version) => ({ + version: version, + supports: version.isAtLeast(1, 28, 0), + message: this.errorMessage('Tenant get method over REST', version.show(), '1.28.0'), + })); + supportsDynamicVectorIndex = () => { return this.dbVersionProvider.getVersion().then((version) => { return { From 1d62717659c941fd95d7fcd9ae1ed62fbfa5253a Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Tue, 1 Apr 2025 14:14:59 +0100 Subject: [PATCH 24/49] Add back tests of unsupported error throwing for <1.25 --- src/collections/tenants/integration.test.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/collections/tenants/integration.test.ts b/src/collections/tenants/integration.test.ts index af3c6c95..b574759a 100644 --- a/src/collections/tenants/integration.test.ts +++ b/src/collections/tenants/integration.test.ts @@ -84,19 +84,34 @@ describe('Testing of the collection.tenants methods', () => { describe('getByName and getByNames', () => { it('should be able to get a tenant by name string', async () => { - const result = await collection.tenants.getByName('hot'); + const query = () => collection.tenants.getByName('hot'); + if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 25, 0))) { + await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); + return; + } + const result = await query(); expect(result).toHaveProperty('name', 'hot'); expect(result).toHaveProperty('activityStatus', 'ACTIVE'); }); it('should be able to get a tenant by tenant object', async () => { - const result = await collection.tenants.getByName({ name: 'hot' }); + const query = () => collection.tenants.getByName({ name: 'hot' }); + if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 25, 0))) { + await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); + return; + } + const result = await query(); expect(result).toHaveProperty('name', 'hot'); expect(result).toHaveProperty('activityStatus', 'ACTIVE'); }); it('should fail to get a non-existing tenant', async () => { - const result = await collection.tenants.getByName('non-existing'); + const query = () => collection.tenants.getByName('non-existing'); + if (await client.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 25, 0))) { + await expect(query()).rejects.toThrow(WeaviateUnsupportedFeatureError); + return; + } + const result = await query(); expect(result).toBeNull(); }); From 2ab578191b4b0ef5fe7b6e4070a801b68d4946af Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Mon, 31 Mar 2025 12:44:43 +0200 Subject: [PATCH 25/49] chore: update OpenAPI schema --- src/openapi/schema.ts | 427 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 424 insertions(+), 3 deletions(-) diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index 598ea51b..b5c11aa5 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -40,9 +40,29 @@ export interface paths { }; }; }; + '/replication/replicate': { + post: operations['replicate']; + }; '/users/own-info': { get: operations['getOwnInfo']; }; + '/users/db': { + get: operations['listAllUsers']; + }; + '/users/db/{user_id}': { + get: operations['getUserInfo']; + post: operations['createUser']; + delete: operations['deleteUser']; + }; + '/users/db/{user_id}/rotate-key': { + post: operations['rotateUserApiKey']; + }; + '/users/db/{user_id}/activate': { + post: operations['activateUser']; + }; + '/users/db/{user_id}/deactivate': { + post: operations['deactivateUser']; + }; '/authz/roles': { get: operations['getRoles']; post: operations['createRole']; @@ -61,9 +81,15 @@ export interface paths { post: operations['hasPermission']; }; '/authz/roles/{id}/users': { + get: operations['getUsersForRoleDeprecated']; + }; + '/authz/roles/{id}/user-assignments': { get: operations['getUsersForRole']; }; '/authz/users/{id}/roles': { + get: operations['getRolesForUserDeprecated']; + }; + '/authz/users/{id}/roles/{userType}': { get: operations['getRolesForUser']; }; '/authz/users/{id}/assign': { @@ -231,6 +257,16 @@ export interface paths { } export interface definitions { + /** + * @description the type of user + * @enum {string} + */ + UserTypeInput: 'db' | 'oidc'; + /** + * @description the type of user + * @enum {string} + */ + UserTypeOutput: 'db_user' | 'db_env_user' | 'oidc'; UserOwnInfo: { /** @description The groups associated to the user */ groups?: string[]; @@ -238,6 +274,23 @@ export interface definitions { /** @description The username associated with the provided key */ username: string; }; + DBUserInfo: { + /** @description The role names associated to the user */ + roles: string[]; + /** @description The user id of the given user */ + userId: string; + /** + * @description type of the returned user + * @enum {string} + */ + dbUserType: 'db_user' | 'db_env_user'; + /** @description activity status of the returned user */ + active: boolean; + }; + UserApiKey: { + /** @description The apikey */ + apikey: string; + }; Role: { /** @description role name */ name: string; @@ -349,7 +402,10 @@ export interface definitions { | 'update_collections' | 'delete_collections' | 'assign_and_revoke_users' + | 'create_users' | 'read_users' + | 'update_users' + | 'delete_users' | 'create_tenants' | 'read_tenants' | 'update_tenants' @@ -371,6 +427,7 @@ export interface definitions { /** @description The username that was extracted either from the authentication information */ username?: string; groups?: string[]; + userType?: definitions['UserTypeInput']; }; /** @description An array of available words and contexts. */ C11yWordsResponse: { @@ -603,6 +660,35 @@ export interface definitions { value?: { [key: string]: unknown }; merge?: definitions['Object']; }; + /** @description Request body to add a replica of given shard of a given collection */ + ReplicationReplicateReplicaRequest: { + /** @description The node containing the replica */ + sourceNodeName: string; + /** @description The node to add a copy of the replica on */ + destinationNodeName: string; + /** @description The collection name holding the shard */ + collectionId: string; + /** @description The shard id holding the replica to be copied */ + shardId: string; + }; + /** @description Request body to disable (soft-delete) a replica of given shard of a given collection */ + ReplicationDisableReplicaRequest: { + /** @description The node containing the replica to be disabled */ + nodeName: string; + /** @description The collection name holding the replica to be disabled */ + collectionId: string; + /** @description The shard id holding the replica to be disabled */ + shardId: string; + }; + /** @description Request body to delete a replica of given shard of a given collection */ + ReplicationDeleteReplicaRequest: { + /** @description The node containing the replica to be deleted */ + nodeName: string; + /** @description The collection name holding the replica to be delete */ + collectionId: string; + /** @description The shard id holding the replica to be deleted */ + shardId: string; + }; /** @description A single peer in the network. */ PeerUpdate: { /** @@ -708,7 +794,8 @@ export interface definitions { | 'trigram' | 'gse' | 'kagome_kr' - | 'kagome_ja'; + | 'kagome_ja' + | 'gse_ch'; /** @description The properties of the nested object(s). Applies to object and object[] data types. */ nestedProperties?: definitions['NestedProperty'][]; }; @@ -736,7 +823,8 @@ export interface definitions { | 'trigram' | 'gse' | 'kagome_kr' - | 'kagome_ja'; + | 'kagome_ja' + | 'gse_ch'; /** @description The properties of the nested object(s). Applies to object and object[] data types. */ nestedProperties?: definitions['NestedProperty'][]; }; @@ -1611,6 +1699,35 @@ export interface operations { 503: unknown; }; }; + replicate: { + parameters: { + body: { + body: definitions['ReplicationReplicateReplicaRequest']; + }; + }; + responses: { + /** Replication operation registered successfully */ + 200: unknown; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; getOwnInfo: { responses: { /** Info about the user */ @@ -1625,6 +1742,229 @@ export interface operations { }; }; }; + listAllUsers: { + responses: { + /** Info about the user */ + 200: { + schema: definitions['DBUserInfo'][]; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + getUserInfo: { + parameters: { + path: { + /** user id */ + user_id: string; + }; + }; + responses: { + /** Info about the user */ + 200: { + schema: definitions['DBUserInfo']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** user not found */ + 404: unknown; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + createUser: { + parameters: { + path: { + /** user id */ + user_id: string; + }; + }; + responses: { + /** User created successfully */ + 201: { + schema: definitions['UserApiKey']; + }; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** User already exists */ + 409: { + schema: definitions['ErrorResponse']; + }; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + deleteUser: { + parameters: { + path: { + /** user name */ + user_id: string; + }; + }; + responses: { + /** Successfully deleted. */ + 204: never; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** user not found */ + 404: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + rotateUserApiKey: { + parameters: { + path: { + /** user id */ + user_id: string; + }; + }; + responses: { + /** ApiKey successfully changed */ + 200: { + schema: definitions['UserApiKey']; + }; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** user not found */ + 404: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + activateUser: { + parameters: { + path: { + /** user id */ + user_id: string; + }; + }; + responses: { + /** User successfully activated */ + 200: unknown; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** user not found */ + 404: unknown; + /** user already activated */ + 409: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + deactivateUser: { + parameters: { + path: { + /** user id */ + user_id: string; + }; + body: { + body?: { + /** + * @description if the key should be revoked when deactivating the user + * @default false + */ + revoke_key?: boolean; + }; + }; + }; + responses: { + /** users successfully deactivated */ + 200: unknown; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** user not found */ + 404: unknown; + /** user already deactivated */ + 409: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. Are you sure the class is defined in the configuration file? */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; getRoles: { responses: { /** Successful response. */ @@ -1849,7 +2189,7 @@ export interface operations { }; }; }; - getUsersForRole: { + getUsersForRoleDeprecated: { parameters: { path: { /** role name */ @@ -1879,11 +2219,86 @@ export interface operations { }; }; }; + getUsersForRole: { + parameters: { + path: { + /** role name */ + id: string; + }; + }; + responses: { + /** Users assigned to this role */ + 200: { + schema: ({ + userId?: string; + userType: definitions['UserTypeOutput']; + } & { + name: unknown; + })[]; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** no role found */ + 404: unknown; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + getRolesForUserDeprecated: { + parameters: { + path: { + /** user name */ + id: string; + }; + }; + responses: { + /** Role assigned users */ + 200: { + schema: definitions['RolesListResponse']; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** no role found for user */ + 404: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. Are you sure the class is defined in the configuration file? */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; getRolesForUser: { parameters: { path: { /** user name */ id: string; + /** The type of user */ + userType: 'oidc' | 'db'; + }; + query: { + /** Whether to include detailed role information needed the roles permission */ + includeFullRoles?: boolean; }; }; responses: { @@ -1903,6 +2318,10 @@ export interface operations { }; /** no role found for user */ 404: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. Are you sure the class is defined in the configuration file? */ + 422: { + schema: definitions['ErrorResponse']; + }; /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ 500: { schema: definitions['ErrorResponse']; @@ -1919,6 +2338,7 @@ export interface operations { body: { /** @description the roles that assigned to user */ roles?: string[]; + userType?: definitions['UserTypeInput']; }; }; }; @@ -1955,6 +2375,7 @@ export interface operations { body: { /** @description the roles that revoked from the key or user */ roles?: string[]; + userType?: definitions['UserTypeInput']; }; }; }; From 1929a2212546b70e80101c3dce31d152a92cbe38 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Mon, 31 Mar 2025 13:40:31 +0200 Subject: [PATCH 26/49] feat: enable dynamic user management, add db/oidc namespaces --- src/classifications/scheduler.ts | 2 +- src/connection/http.ts | 78 +++++++++--------- src/openapi/schema.ts | 132 +++++++++++++++---------------- src/openapi/types.ts | 4 + src/roles/util.ts | 18 ++++- src/users/index.ts | 129 ++++++++++++++++++++++++++---- src/users/types.ts | 10 +++ 7 files changed, 252 insertions(+), 121 deletions(-) diff --git a/src/classifications/scheduler.ts b/src/classifications/scheduler.ts index a46045e3..c089eb88 100644 --- a/src/classifications/scheduler.ts +++ b/src/classifications/scheduler.ts @@ -101,7 +101,7 @@ export default class ClassificationsScheduler extends CommandBase { reject( new Error( "classification didn't finish within configured timeout, " + - 'set larger timeout with .withWaitTimeout(timeout)' + 'set larger timeout with .withWaitTimeout(timeout)' ) ); }, this.waitTimeout); diff --git a/src/connection/http.ts b/src/connection/http.ts index 5250d930..9c123644 100644 --- a/src/connection/http.ts +++ b/src/connection/http.ts @@ -116,10 +116,14 @@ export default class ConnectionREST { postReturn = (path: string, payload: B): Promise => { if (this.authEnabled) { return this.login().then((token) => - this.http.post(path, payload, true, token).then((res) => res as T) + this.http.post(path, payload, true, token) as T ); } - return this.http.post(path, payload, true, '').then((res) => res as T); + return this.http.post(path, payload, true, '') as Promise; + }; + + postNoBody = (path: string): Promise => { + return this.postReturn(path, null); }; postEmpty = (path: string, payload: B): Promise => { @@ -372,46 +376,46 @@ const makeUrl = (basePath: string) => (path: string) => basePath + path; const checkStatus = (expectResponseBody: boolean) => - (res: Response) => { - if (res.status >= 400) { - return res.text().then((errText: string) => { - let err: string; - try { - // in case of invalid json response (like empty string) - err = JSON.stringify(JSON.parse(errText)); - } catch (e) { - err = errText; - } - if (res.status === 401) { - return Promise.reject(new WeaviateUnauthenticatedError(err)); - } else if (res.status === 403) { - return Promise.reject(new WeaviateInsufficientPermissionsError(403, err)); - } else { - return Promise.reject(new WeaviateUnexpectedStatusCodeError(res.status, err)); - } - }); - } - if (expectResponseBody) { - return res.json() as Promise; - } - return Promise.resolve(undefined); - }; + (res: Response) => { + if (res.status >= 400) { + return res.text().then((errText: string) => { + let err: string; + try { + // in case of invalid json response (like empty string) + err = JSON.stringify(JSON.parse(errText)); + } catch (e) { + err = errText; + } + if (res.status === 401) { + return Promise.reject(new WeaviateUnauthenticatedError(err)); + } else if (res.status === 403) { + return Promise.reject(new WeaviateInsufficientPermissionsError(403, err)); + } else { + return Promise.reject(new WeaviateUnexpectedStatusCodeError(res.status, err)); + } + }); + } + if (expectResponseBody) { + return res.json() as Promise; + } + return Promise.resolve(undefined); + }; const handleHeadResponse = (expectResponseBody: boolean) => - (res: Response) => { - if (res.status == 200 || res.status == 204 || res.status == 404) { - return Promise.resolve(res.status == 200 || res.status == 204); - } - return checkStatus(expectResponseBody)(res); - }; + (res: Response) => { + if (res.status == 200 || res.status == 204 || res.status == 404) { + return Promise.resolve(res.status == 200 || res.status == 204); + } + return checkStatus(expectResponseBody)(res); + }; const getAuthHeaders = (config: InternalConnectionParams, bearerToken: string) => bearerToken ? { - Authorization: `Bearer ${bearerToken}`, - 'X-Weaviate-Cluster-Url': config.host, - // keeping for backwards compatibility for older clusters for now. On newer clusters, Embedding Service reuses Authorization header. - 'X-Weaviate-Api-Key': bearerToken, - } + Authorization: `Bearer ${bearerToken}`, + 'X-Weaviate-Cluster-Url': config.host, + // keeping for backwards compatibility for older clusters for now. On newer clusters, Embedding Service reuses Authorization header. + 'X-Weaviate-Api-Key': bearerToken, + } : undefined; diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index b5c11aa5..c3dabef0 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -386,30 +386,30 @@ export interface definitions { * @enum {string} */ action: - | 'manage_backups' - | 'read_cluster' - | 'create_data' - | 'read_data' - | 'update_data' - | 'delete_data' - | 'read_nodes' - | 'create_roles' - | 'read_roles' - | 'update_roles' - | 'delete_roles' - | 'create_collections' - | 'read_collections' - | 'update_collections' - | 'delete_collections' - | 'assign_and_revoke_users' - | 'create_users' - | 'read_users' - | 'update_users' - | 'delete_users' - | 'create_tenants' - | 'read_tenants' - | 'update_tenants' - | 'delete_tenants'; + | 'manage_backups' + | 'read_cluster' + | 'create_data' + | 'read_data' + | 'update_data' + | 'delete_data' + | 'read_nodes' + | 'create_roles' + | 'read_roles' + | 'update_roles' + | 'delete_roles' + | 'create_collections' + | 'read_collections' + | 'update_collections' + | 'delete_collections' + | 'assign_and_revoke_users' + | 'create_users' + | 'read_users' + | 'update_users' + | 'delete_users' + | 'create_tenants' + | 'read_tenants' + | 'update_tenants' + | 'delete_tenants'; }; /** @description list of roles */ RolesListResponse: definitions['Role'][]; @@ -787,15 +787,15 @@ export interface definitions { * @enum {string} */ tokenization?: - | 'word' - | 'lowercase' - | 'whitespace' - | 'field' - | 'trigram' - | 'gse' - | 'kagome_kr' - | 'kagome_ja' - | 'gse_ch'; + | 'word' + | 'lowercase' + | 'whitespace' + | 'field' + | 'trigram' + | 'gse' + | 'kagome_kr' + | 'kagome_ja' + | 'gse_ch'; /** @description The properties of the nested object(s). Applies to object and object[] data types. */ nestedProperties?: definitions['NestedProperty'][]; }; @@ -816,15 +816,15 @@ export interface definitions { indexRangeFilters?: boolean; /** @enum {string} */ tokenization?: - | 'word' - | 'lowercase' - | 'whitespace' - | 'field' - | 'trigram' - | 'gse' - | 'kagome_kr' - | 'kagome_ja' - | 'gse_ch'; + | 'word' + | 'lowercase' + | 'whitespace' + | 'field' + | 'trigram' + | 'gse' + | 'kagome_kr' + | 'kagome_ja' + | 'gse_ch'; /** @description The properties of the nested object(s). Applies to object and object[] data types. */ nestedProperties?: definitions['NestedProperty'][]; }; @@ -1505,19 +1505,19 @@ export interface definitions { * @enum {string} */ operator?: - | 'And' - | 'Or' - | 'Equal' - | 'Like' - | 'NotEqual' - | 'GreaterThan' - | 'GreaterThanEqual' - | 'LessThan' - | 'LessThanEqual' - | 'WithinGeoRange' - | 'IsNull' - | 'ContainsAny' - | 'ContainsAll'; + | 'And' + | 'Or' + | 'Equal' + | 'Like' + | 'NotEqual' + | 'GreaterThan' + | 'GreaterThanEqual' + | 'LessThan' + | 'LessThanEqual' + | 'WithinGeoRange' + | 'IsNull' + | 'ContainsAny' + | 'ContainsAll'; /** * @description path to the property currently being filtered * @example [ @@ -1618,16 +1618,16 @@ export interface definitions { * @enum {string} */ activityStatus?: - | 'ACTIVE' - | 'INACTIVE' - | 'OFFLOADED' - | 'OFFLOADING' - | 'ONLOADING' - | 'HOT' - | 'COLD' - | 'FROZEN' - | 'FREEZING' - | 'UNFREEZING'; + | 'ACTIVE' + | 'INACTIVE' + | 'OFFLOADED' + | 'OFFLOADING' + | 'ONLOADING' + | 'HOT' + | 'COLD' + | 'FROZEN' + | 'FREEZING' + | 'UNFREEZING'; }; /** @description attributes representing a single tenant response within weaviate */ TenantResponse: definitions['Tenant'] & { @@ -4187,4 +4187,4 @@ export interface operations { }; } -export interface external {} +export interface external { } diff --git a/src/openapi/types.ts b/src/openapi/types.ts index 054ab10d..4924fc90 100644 --- a/src/openapi/types.ts +++ b/src/openapi/types.ts @@ -55,6 +55,10 @@ export type WeaviateReplicationConfig = WeaviateClass['replicationConfig']; export type WeaviateShardingConfig = WeaviateClass['shardingConfig']; export type WeaviateShardStatus = definitions['ShardStatusGetResponse']; export type WeaviateUser = definitions['UserOwnInfo']; +export type WeaviateDBUser = definitions['DBUserInfo']; +export type WeaviateUserType = definitions['UserTypeOutput']; +export type WeaviateUserTypeInternal = definitions['UserTypeInput']; +export type WeaviateUserTypeDB = definitions['DBUserInfo']['dbUserType']; export type WeaviateVectorIndexConfig = WeaviateClass['vectorIndexConfig']; export type WeaviateVectorsConfig = WeaviateClass['vectorConfig']; export type WeaviateVectorConfig = definitions['VectorConfig']; diff --git a/src/roles/util.ts b/src/roles/util.ts index bee179db..dad7159e 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -1,5 +1,5 @@ -import { Permission as WeaviatePermission, Role as WeaviateRole, WeaviateUser } from '../openapi/types.js'; -import { User } from '../users/types.js'; +import { WeaviateDBUser, Permission as WeaviatePermission, Role as WeaviateRole, WeaviateUser } from '../openapi/types.js'; +import { User, UserDB } from '../users/types.js'; import { BackupsAction, BackupsPermission, @@ -136,7 +136,19 @@ export class Map { static user = (user: WeaviateUser): User => ({ id: user.username, roles: user.roles?.map(Map.roleFromWeaviate), - }); + }) + static dbUser = (user: WeaviateDBUser): UserDB => ({ + userType: user.dbUserType, + id: user.userId, + roleNames: user.roles, + active: user.active, + }) + static dbUsers = (users: WeaviateDBUser[]): UserDB[] => + users.reduce((acc, user) => { + acc.push(Map.dbUser(user)); + return acc; + }, [] as UserDB[]) + ; } class PermissionsMapping { diff --git a/src/users/index.ts b/src/users/index.ts index 353909fc..79d9b0b9 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -1,10 +1,33 @@ import { ConnectionREST } from '../index.js'; -import { Role as WeaviateRole, WeaviateUser } from '../openapi/types.js'; +import { Role as WeaviateRole, WeaviateUserTypeInternal as UserTypeInternal, WeaviateUser, WeaviateDBUser } from '../openapi/types.js'; import { Role } from '../roles/types.js'; import { Map } from '../roles/util.js'; -import { User } from './types.js'; +import { User, UserDB } from './types.js'; -export interface Users { +/** +* Operations supported for 'db', 'oidc', and legacy (non-namespaced) users. +* Use respective implementations in `users.db` and `users.oidc`, and `users`. +*/ +interface UsersBase { + /** + * Assign roles to a user. + * + * @param {string | string[]} roleNames The name or names of the roles to assign. + * @param {string} userId The ID of the user to assign the roles to. + * @returns {Promise} A promise that resolves when the roles are assigned. + */ + assignRoles: (roleNames: string | string[], userId: string) => Promise; + /** + * Revoke roles from a user. + * + * @param {string | string[]} roleNames The name or names of the roles to revoke. + * @param {string} userId The ID of the user to revoke the roles from. + * @returns {Promise} A promise that resolves when the roles are revoked. + */ + revokeRoles: (roleNames: string | string[], userId: string) => Promise; +} + +export interface Users extends UsersBase { /** * Retrieve the information relevant to the currently authenticated user. * @@ -18,38 +41,116 @@ export interface Users { * @returns {Promise>} A map of role names to their respective roles. */ getAssignedRoles: (userId: string) => Promise>; + + db: DBUsers, + oidc: OIDCUsers, +} + +/** Operations supported for namespaced 'db' users.*/ +export interface DBUsers extends UsersBase { /** - * Assign roles to a user. + * Retrieve the roles assigned to a user. * - * @param {string | string[]} roleNames The name or names of the roles to assign. - * @param {string} userId The ID of the user to assign the roles to. - * @returns {Promise} A promise that resolves when the roles are assigned. + * @param {string} userId The ID of the user to retrieve the assigned roles for. + * @returns {Promise>} A map of role names to their respective roles. */ - assignRoles: (roleNames: string | string[], userId: string) => Promise; + getAssignedRoles: (userId: string, includePermissions?: boolean) => Promise>; + + create: (userId: string) => Promise; + delete: (userId: string) => Promise; + rotateKey: (userId: string) => Promise; + activate: (userId: string) => Promise; + deactivate: (userId: string) => Promise; + byName: (userId: string) => Promise; + listAll: () => Promise; +} + +/** Operations supported for namespaced 'oidc' users.*/ +export interface OIDCUsers extends UsersBase { /** - * Revoke roles from a user. + * Retrieve the roles assigned to a user. * - * @param {string | string[]} roleNames The name or names of the roles to revoke. - * @param {string} userId The ID of the user to revoke the roles from. - * @returns {Promise} A promise that resolves when the roles are revoked. + * @param {string} userId The ID of the user to retrieve the assigned roles for. + * @returns {Promise>} A map of role names to their respective roles. */ - revokeRoles: (roleNames: string | string[], userId: string) => Promise; + getAssignedRoles: (userId: string, includePermissions?: boolean) => Promise>; } const users = (connection: ConnectionREST): Users => { + const ns = namespaced(connection); + return { getMyUser: () => connection.get('/users/own-info').then(Map.user), getAssignedRoles: (userId: string) => connection.get(`/authz/users/${userId}/roles`).then(Map.roles), assignRoles: (roleNames: string | string[], userId: string) => + ns.assignRoles(roleNames, userId), + revokeRoles: (roleNames: string | string[], userId: string) => + ns.revokeRoles(roleNames, userId), + db: db(connection), + oidc: oidc(connection), + }; +}; + +const db = (connection: ConnectionREST): DBUsers => { + const ns = namespaced(connection); + + type APIKeyResponse = { apiKey: string }; + return { + getAssignedRoles: (userId: string, includePermissions?: boolean) => ns.getAssignedRoles(userId, 'db', includePermissions), + assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, 'db'), + revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, 'db'), + + create: (userId: string) => connection.postNoBody(`/users/db/${userId}`) + .then(resp => resp.apiKey), + delete: (userId: string) => connection.delete(`/users/db/${userId}`, null) + .then(() => true).catch(() => false), + rotateKey: (userId: string) => connection.postNoBody(`users/db/${userId}/rotate-key`) + .then(resp => resp.apiKey), + activate: (userId: string) => connection.postNoBody(`/users/db/${userId}/activate`) + .then(() => true).catch(reason => reason.code !== undefined ? reason.code === 409 : false), + deactivate: (userId: string) => connection.postNoBody(`/users/db/${userId}/deactivate`) + .then(() => true).catch(reason => reason.code !== undefined ? reason.code === 409 : false), + byName: (userId: string) => connection.get(`/users/db/${userId}`, true).then(Map.dbUser), + listAll: () => connection.get('/users/db', true).then(Map.dbUsers), + }; +} + +const oidc = (connection: ConnectionREST): OIDCUsers => { + const ns = namespaced(connection); + return { + getAssignedRoles: (userId: string, includePermissions?: boolean) => ns.getAssignedRoles(userId, 'oidc', includePermissions), + assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, 'oidc'), + revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, 'oidc'), + }; +} + +// TODO: see if we can extend definitions of UsersBase with additional UserType arg +/** Internal interface for operations that MAY accept a 'db'/'oidc' namespace. */ +interface NamespacedUsers { + getAssignedRoles: (userId: string, userType: UserTypeInternal, includePermissions?: boolean) => Promise>; + assignRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => Promise; + revokeRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => Promise; +} + +const namespaced = (connection: ConnectionREST): NamespacedUsers => { + return { + getAssignedRoles: (userId: string, userType: UserTypeInternal, includePermissions?: boolean) => + connection.get( + `/authz/users/${userId}/roles/${userType}${includePermissions ? '?&includeFullRoles=true' : ''}` + ).then(Map.roles), + assignRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => connection.postEmpty(`/authz/users/${userId}/assign`, { roles: Array.isArray(roleNames) ? roleNames : [roleNames], + userType: userType }), - revokeRoles: (roleNames: string | string[], userId: string) => + revokeRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => connection.postEmpty(`/authz/users/${userId}/revoke`, { roles: Array.isArray(roleNames) ? roleNames : [roleNames], + userType: userType, }), }; }; + export default users; diff --git a/src/users/types.ts b/src/users/types.ts index 097b1c57..a14dce06 100644 --- a/src/users/types.ts +++ b/src/users/types.ts @@ -1,6 +1,16 @@ import { Role } from '../roles/types.js'; +import { WeaviateUserTypeDB as UserTypeDB } from '../openapi/types.js'; export type User = { id: string; roles?: Role[]; }; + +export type UserDB = { + userType: UserTypeDB; + id: string; + roleNames: string[]; + active: boolean; +}; + + From a9cf76c8df81b2b9027f8bd6eade4c9d7709ef05 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Mon, 31 Mar 2025 14:33:58 +0200 Subject: [PATCH 27/49] refactor: re-use common code snippets --- src/users/index.ts | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index 79d9b0b9..b6410995 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -1,5 +1,6 @@ import { ConnectionREST } from '../index.js'; import { Role as WeaviateRole, WeaviateUserTypeInternal as UserTypeInternal, WeaviateUser, WeaviateDBUser } from '../openapi/types.js'; +import roles from '../roles/index.js'; import { Role } from '../roles/types.js'; import { Map } from '../roles/util.js'; import { User, UserDB } from './types.js'; @@ -77,27 +78,31 @@ export interface OIDCUsers extends UsersBase { } const users = (connection: ConnectionREST): Users => { - const ns = namespaced(connection); + const base = baseUsers(connection); return { getMyUser: () => connection.get('/users/own-info').then(Map.user), getAssignedRoles: (userId: string) => connection.get(`/authz/users/${userId}/roles`).then(Map.roles), assignRoles: (roleNames: string | string[], userId: string) => - ns.assignRoles(roleNames, userId), + base.assignRoles(roleNames, userId), revokeRoles: (roleNames: string | string[], userId: string) => - ns.revokeRoles(roleNames, userId), + base.revokeRoles(roleNames, userId), db: db(connection), oidc: oidc(connection), }; }; const db = (connection: ConnectionREST): DBUsers => { - const ns = namespaced(connection); + const ns = namespacedUsers(connection); + + const allowCode = (code: number): (reason: any) => boolean => { + return reason => reason.code !== undefined && reason.code === code; + } type APIKeyResponse = { apiKey: string }; return { - getAssignedRoles: (userId: string, includePermissions?: boolean) => ns.getAssignedRoles(userId, 'db', includePermissions), + getAssignedRoles: (userId: string, includePermissions?: boolean) => ns.getAssignedRoles('db', userId, includePermissions), assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, 'db'), revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, 'db'), @@ -108,41 +113,50 @@ const db = (connection: ConnectionREST): DBUsers => { rotateKey: (userId: string) => connection.postNoBody(`users/db/${userId}/rotate-key`) .then(resp => resp.apiKey), activate: (userId: string) => connection.postNoBody(`/users/db/${userId}/activate`) - .then(() => true).catch(reason => reason.code !== undefined ? reason.code === 409 : false), + .then(() => true).catch(allowCode(409)), deactivate: (userId: string) => connection.postNoBody(`/users/db/${userId}/deactivate`) - .then(() => true).catch(reason => reason.code !== undefined ? reason.code === 409 : false), + .then(() => true).catch(allowCode(409)), byName: (userId: string) => connection.get(`/users/db/${userId}`, true).then(Map.dbUser), listAll: () => connection.get('/users/db', true).then(Map.dbUsers), }; } const oidc = (connection: ConnectionREST): OIDCUsers => { - const ns = namespaced(connection); + const ns = namespacedUsers(connection); return { - getAssignedRoles: (userId: string, includePermissions?: boolean) => ns.getAssignedRoles(userId, 'oidc', includePermissions), + getAssignedRoles: (userId: string, includePermissions?: boolean) => ns.getAssignedRoles('oidc', userId, includePermissions), assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, 'oidc'), revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, 'oidc'), }; } -// TODO: see if we can extend definitions of UsersBase with additional UserType arg /** Internal interface for operations that MAY accept a 'db'/'oidc' namespace. */ interface NamespacedUsers { - getAssignedRoles: (userId: string, userType: UserTypeInternal, includePermissions?: boolean) => Promise>; + getAssignedRoles: (userType: UserTypeInternal, userId: string, includePermissions?: boolean) => Promise>; assignRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => Promise; revokeRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => Promise; } -const namespaced = (connection: ConnectionREST): NamespacedUsers => { +const baseUsers = (connection: ConnectionREST): UsersBase => { + const ns = namespacedUsers(connection); + return { + assignRoles: (roleNames: string | string[], userId: string) => + ns.assignRoles(roleNames, userId), + revokeRoles: (roleNames: string | string[], userId: string) => + ns.revokeRoles(roleNames, userId), + }; +} + +const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { return { - getAssignedRoles: (userId: string, userType: UserTypeInternal, includePermissions?: boolean) => + getAssignedRoles: (userType: UserTypeInternal, userId: string, includePermissions?: boolean) => connection.get( `/authz/users/${userId}/roles/${userType}${includePermissions ? '?&includeFullRoles=true' : ''}` ).then(Map.roles), assignRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => connection.postEmpty(`/authz/users/${userId}/assign`, { roles: Array.isArray(roleNames) ? roleNames : [roleNames], - userType: userType + userType: userType, }), revokeRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => connection.postEmpty(`/authz/users/${userId}/revoke`, { From fd3ff062b7a03234374a1689840dc606bb9eb6b3 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Mon, 31 Mar 2025 17:54:46 +0200 Subject: [PATCH 28/49] test: extend integration test suite --- .github/workflows/main.yaml | 4 +- ci/compose.sh | 0 ci/docker-compose-rbac.yml | 6 ++ src/classifications/scheduler.ts | 2 +- src/connection/http.ts | 74 +++++++++-------- src/openapi/schema.ts | 132 +++++++++++++++---------------- src/roles/util.ts | 20 +++-- src/users/index.ts | 94 +++++++++++++--------- src/users/integration.test.ts | 104 +++++++++++++++++++++++- src/users/types.ts | 4 +- 10 files changed, 281 insertions(+), 159 deletions(-) mode change 100644 => 100755 ci/compose.sh diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 6baf3620..c65e7f40 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -18,7 +18,7 @@ env: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true - + jobs: checks: runs-on: ubuntu-latest @@ -139,4 +139,4 @@ jobs: uses: softprops/action-gh-release@v1 with: generate_release_notes: true - draft: true \ No newline at end of file + draft: true diff --git a/ci/compose.sh b/ci/compose.sh old mode 100644 new mode 100755 diff --git a/ci/docker-compose-rbac.yml b/ci/docker-compose-rbac.yml index 57f2b13a..6091f498 100644 --- a/ci/docker-compose-rbac.yml +++ b/ci/docker-compose-rbac.yml @@ -28,4 +28,10 @@ services: AUTHORIZATION_RBAC_ENABLED: "true" AUTHORIZATION_ADMIN_USERS: "admin-user" AUTHORIZATION_VIEWER_USERS: "viewer-user" + AUTHENTICATION_DB_USERS_ENABLED: "true" + AUTHENTICATION_OIDC_ENABLED: "true" + AUTHENTICATION_OIDC_CLIENT_ID: "wcs" + AUTHENTICATION_OIDC_ISSUER: "https://auth.wcs.api.weaviate.io/auth/realms/SeMI" + AUTHENTICATION_OIDC_USERNAME_CLAIM: "email" + AUTHENTICATION_OIDC_GROUPS_CLAIM: "groups" ... diff --git a/src/classifications/scheduler.ts b/src/classifications/scheduler.ts index c089eb88..a46045e3 100644 --- a/src/classifications/scheduler.ts +++ b/src/classifications/scheduler.ts @@ -101,7 +101,7 @@ export default class ClassificationsScheduler extends CommandBase { reject( new Error( "classification didn't finish within configured timeout, " + - 'set larger timeout with .withWaitTimeout(timeout)' + 'set larger timeout with .withWaitTimeout(timeout)' ) ); }, this.waitTimeout); diff --git a/src/connection/http.ts b/src/connection/http.ts index 9c123644..621a3f3b 100644 --- a/src/connection/http.ts +++ b/src/connection/http.ts @@ -115,9 +115,7 @@ export default class ConnectionREST { postReturn = (path: string, payload: B): Promise => { if (this.authEnabled) { - return this.login().then((token) => - this.http.post(path, payload, true, token) as T - ); + return this.login().then((token) => this.http.post(path, payload, true, token) as T); } return this.http.post(path, payload, true, '') as Promise; }; @@ -376,46 +374,46 @@ const makeUrl = (basePath: string) => (path: string) => basePath + path; const checkStatus = (expectResponseBody: boolean) => - (res: Response) => { - if (res.status >= 400) { - return res.text().then((errText: string) => { - let err: string; - try { - // in case of invalid json response (like empty string) - err = JSON.stringify(JSON.parse(errText)); - } catch (e) { - err = errText; - } - if (res.status === 401) { - return Promise.reject(new WeaviateUnauthenticatedError(err)); - } else if (res.status === 403) { - return Promise.reject(new WeaviateInsufficientPermissionsError(403, err)); - } else { - return Promise.reject(new WeaviateUnexpectedStatusCodeError(res.status, err)); - } - }); - } - if (expectResponseBody) { - return res.json() as Promise; - } - return Promise.resolve(undefined); - }; + (res: Response) => { + if (res.status >= 400) { + return res.text().then((errText: string) => { + let err: string; + try { + // in case of invalid json response (like empty string) + err = JSON.stringify(JSON.parse(errText)); + } catch (e) { + err = errText; + } + if (res.status === 401) { + return Promise.reject(new WeaviateUnauthenticatedError(err)); + } else if (res.status === 403) { + return Promise.reject(new WeaviateInsufficientPermissionsError(403, err)); + } else { + return Promise.reject(new WeaviateUnexpectedStatusCodeError(res.status, err)); + } + }); + } + if (expectResponseBody) { + return res.json() as Promise; + } + return Promise.resolve(undefined); + }; const handleHeadResponse = (expectResponseBody: boolean) => - (res: Response) => { - if (res.status == 200 || res.status == 204 || res.status == 404) { - return Promise.resolve(res.status == 200 || res.status == 204); - } - return checkStatus(expectResponseBody)(res); - }; + (res: Response) => { + if (res.status == 200 || res.status == 204 || res.status == 404) { + return Promise.resolve(res.status == 200 || res.status == 204); + } + return checkStatus(expectResponseBody)(res); + }; const getAuthHeaders = (config: InternalConnectionParams, bearerToken: string) => bearerToken ? { - Authorization: `Bearer ${bearerToken}`, - 'X-Weaviate-Cluster-Url': config.host, - // keeping for backwards compatibility for older clusters for now. On newer clusters, Embedding Service reuses Authorization header. - 'X-Weaviate-Api-Key': bearerToken, - } + Authorization: `Bearer ${bearerToken}`, + 'X-Weaviate-Cluster-Url': config.host, + // keeping for backwards compatibility for older clusters for now. On newer clusters, Embedding Service reuses Authorization header. + 'X-Weaviate-Api-Key': bearerToken, + } : undefined; diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index c3dabef0..b5c11aa5 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -386,30 +386,30 @@ export interface definitions { * @enum {string} */ action: - | 'manage_backups' - | 'read_cluster' - | 'create_data' - | 'read_data' - | 'update_data' - | 'delete_data' - | 'read_nodes' - | 'create_roles' - | 'read_roles' - | 'update_roles' - | 'delete_roles' - | 'create_collections' - | 'read_collections' - | 'update_collections' - | 'delete_collections' - | 'assign_and_revoke_users' - | 'create_users' - | 'read_users' - | 'update_users' - | 'delete_users' - | 'create_tenants' - | 'read_tenants' - | 'update_tenants' - | 'delete_tenants'; + | 'manage_backups' + | 'read_cluster' + | 'create_data' + | 'read_data' + | 'update_data' + | 'delete_data' + | 'read_nodes' + | 'create_roles' + | 'read_roles' + | 'update_roles' + | 'delete_roles' + | 'create_collections' + | 'read_collections' + | 'update_collections' + | 'delete_collections' + | 'assign_and_revoke_users' + | 'create_users' + | 'read_users' + | 'update_users' + | 'delete_users' + | 'create_tenants' + | 'read_tenants' + | 'update_tenants' + | 'delete_tenants'; }; /** @description list of roles */ RolesListResponse: definitions['Role'][]; @@ -787,15 +787,15 @@ export interface definitions { * @enum {string} */ tokenization?: - | 'word' - | 'lowercase' - | 'whitespace' - | 'field' - | 'trigram' - | 'gse' - | 'kagome_kr' - | 'kagome_ja' - | 'gse_ch'; + | 'word' + | 'lowercase' + | 'whitespace' + | 'field' + | 'trigram' + | 'gse' + | 'kagome_kr' + | 'kagome_ja' + | 'gse_ch'; /** @description The properties of the nested object(s). Applies to object and object[] data types. */ nestedProperties?: definitions['NestedProperty'][]; }; @@ -816,15 +816,15 @@ export interface definitions { indexRangeFilters?: boolean; /** @enum {string} */ tokenization?: - | 'word' - | 'lowercase' - | 'whitespace' - | 'field' - | 'trigram' - | 'gse' - | 'kagome_kr' - | 'kagome_ja' - | 'gse_ch'; + | 'word' + | 'lowercase' + | 'whitespace' + | 'field' + | 'trigram' + | 'gse' + | 'kagome_kr' + | 'kagome_ja' + | 'gse_ch'; /** @description The properties of the nested object(s). Applies to object and object[] data types. */ nestedProperties?: definitions['NestedProperty'][]; }; @@ -1505,19 +1505,19 @@ export interface definitions { * @enum {string} */ operator?: - | 'And' - | 'Or' - | 'Equal' - | 'Like' - | 'NotEqual' - | 'GreaterThan' - | 'GreaterThanEqual' - | 'LessThan' - | 'LessThanEqual' - | 'WithinGeoRange' - | 'IsNull' - | 'ContainsAny' - | 'ContainsAll'; + | 'And' + | 'Or' + | 'Equal' + | 'Like' + | 'NotEqual' + | 'GreaterThan' + | 'GreaterThanEqual' + | 'LessThan' + | 'LessThanEqual' + | 'WithinGeoRange' + | 'IsNull' + | 'ContainsAny' + | 'ContainsAll'; /** * @description path to the property currently being filtered * @example [ @@ -1618,16 +1618,16 @@ export interface definitions { * @enum {string} */ activityStatus?: - | 'ACTIVE' - | 'INACTIVE' - | 'OFFLOADED' - | 'OFFLOADING' - | 'ONLOADING' - | 'HOT' - | 'COLD' - | 'FROZEN' - | 'FREEZING' - | 'UNFREEZING'; + | 'ACTIVE' + | 'INACTIVE' + | 'OFFLOADED' + | 'OFFLOADING' + | 'ONLOADING' + | 'HOT' + | 'COLD' + | 'FROZEN' + | 'FREEZING' + | 'UNFREEZING'; }; /** @description attributes representing a single tenant response within weaviate */ TenantResponse: definitions['Tenant'] & { @@ -4187,4 +4187,4 @@ export interface operations { }; } -export interface external { } +export interface external {} diff --git a/src/roles/util.ts b/src/roles/util.ts index dad7159e..b6ff39d2 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -1,4 +1,9 @@ -import { WeaviateDBUser, Permission as WeaviatePermission, Role as WeaviateRole, WeaviateUser } from '../openapi/types.js'; +import { + WeaviateDBUser, + Permission as WeaviatePermission, + Role as WeaviateRole, + WeaviateUser, +} from '../openapi/types.js'; import { User, UserDB } from '../users/types.js'; import { BackupsAction, @@ -136,19 +141,18 @@ export class Map { static user = (user: WeaviateUser): User => ({ id: user.username, roles: user.roles?.map(Map.roleFromWeaviate), - }) + }); static dbUser = (user: WeaviateDBUser): UserDB => ({ userType: user.dbUserType, id: user.userId, roleNames: user.roles, active: user.active, - }) + }); static dbUsers = (users: WeaviateDBUser[]): UserDB[] => users.reduce((acc, user) => { acc.push(Map.dbUser(user)); return acc; - }, [] as UserDB[]) - ; + }, [] as UserDB[]); } class PermissionsMapping { @@ -172,7 +176,11 @@ class PermissionsMapping { public static use = (role: WeaviateRole) => new PermissionsMapping(role); public map = (): Role => { - this.role.permissions.forEach(this.permissionFromWeaviate); + // If truncated roles are requested (?includeFullRoles=false), + // role.permissions are not present. + if (this.role.permissions !== null) { + this.role.permissions.forEach(this.permissionFromWeaviate); + } return { name: this.role.name, backupsPermissions: Object.values(this.mappings.backups), diff --git a/src/users/index.ts b/src/users/index.ts index b6410995..5e2b9a35 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -1,14 +1,18 @@ import { ConnectionREST } from '../index.js'; -import { Role as WeaviateRole, WeaviateUserTypeInternal as UserTypeInternal, WeaviateUser, WeaviateDBUser } from '../openapi/types.js'; -import roles from '../roles/index.js'; +import { + WeaviateUserTypeInternal as UserTypeInternal, + WeaviateDBUser, + Role as WeaviateRole, + WeaviateUser, +} from '../openapi/types.js'; import { Role } from '../roles/types.js'; import { Map } from '../roles/util.js'; import { User, UserDB } from './types.js'; /** -* Operations supported for 'db', 'oidc', and legacy (non-namespaced) users. -* Use respective implementations in `users.db` and `users.oidc`, and `users`. -*/ + * Operations supported for 'db', 'oidc', and legacy (non-namespaced) users. + * Use respective implementations in `users.db` and `users.oidc`, and `users`. + */ interface UsersBase { /** * Assign roles to a user. @@ -43,8 +47,8 @@ export interface Users extends UsersBase { */ getAssignedRoles: (userId: string) => Promise>; - db: DBUsers, - oidc: OIDCUsers, + db: DBUsers; + oidc: OIDCUsers; } /** Operations supported for namespaced 'db' users.*/ @@ -84,10 +88,8 @@ const users = (connection: ConnectionREST): Users => { getMyUser: () => connection.get('/users/own-info').then(Map.user), getAssignedRoles: (userId: string) => connection.get(`/authz/users/${userId}/roles`).then(Map.roles), - assignRoles: (roleNames: string | string[], userId: string) => - base.assignRoles(roleNames, userId), - revokeRoles: (roleNames: string | string[], userId: string) => - base.revokeRoles(roleNames, userId), + assignRoles: (roleNames: string | string[], userId: string) => base.assignRoles(roleNames, userId), + revokeRoles: (roleNames: string | string[], userId: string) => base.revokeRoles(roleNames, userId), db: db(connection), oidc: oidc(connection), }; @@ -96,43 +98,58 @@ const users = (connection: ConnectionREST): Users => { const db = (connection: ConnectionREST): DBUsers => { const ns = namespacedUsers(connection); - const allowCode = (code: number): (reason: any) => boolean => { - return reason => reason.code !== undefined && reason.code === code; - } + const allowCode = (code: number): ((reason: any) => boolean) => { + return (reason) => reason.code !== undefined && reason.code === code; + }; - type APIKeyResponse = { apiKey: string }; + type APIKeyResponse = { apikey: string }; return { - getAssignedRoles: (userId: string, includePermissions?: boolean) => ns.getAssignedRoles('db', userId, includePermissions), + getAssignedRoles: (userId: string, includePermissions?: boolean) => + ns.getAssignedRoles('db', userId, includePermissions), assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, 'db'), revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, 'db'), - create: (userId: string) => connection.postNoBody(`/users/db/${userId}`) - .then(resp => resp.apiKey), - delete: (userId: string) => connection.delete(`/users/db/${userId}`, null) - .then(() => true).catch(() => false), - rotateKey: (userId: string) => connection.postNoBody(`users/db/${userId}/rotate-key`) - .then(resp => resp.apiKey), - activate: (userId: string) => connection.postNoBody(`/users/db/${userId}/activate`) - .then(() => true).catch(allowCode(409)), - deactivate: (userId: string) => connection.postNoBody(`/users/db/${userId}/deactivate`) - .then(() => true).catch(allowCode(409)), + create: (userId: string) => + connection.postNoBody(`/users/db/${userId}`).then((resp) => resp.apikey), + delete: (userId: string) => + connection + .delete(`/users/db/${userId}`, null) + .then(() => true) + .catch(() => false), + rotateKey: (userId: string) => + connection.postNoBody(`/users/db/${userId}/rotate-key`).then((resp) => resp.apikey), + activate: (userId: string) => + connection + .postNoBody(`/users/db/${userId}/activate`) + .then(() => true) + .catch(allowCode(409)), + deactivate: (userId: string) => + connection + .postNoBody(`/users/db/${userId}/deactivate`) + .then(() => true) + .catch(allowCode(409)), byName: (userId: string) => connection.get(`/users/db/${userId}`, true).then(Map.dbUser), listAll: () => connection.get('/users/db', true).then(Map.dbUsers), }; -} +}; const oidc = (connection: ConnectionREST): OIDCUsers => { const ns = namespacedUsers(connection); return { - getAssignedRoles: (userId: string, includePermissions?: boolean) => ns.getAssignedRoles('oidc', userId, includePermissions), + getAssignedRoles: (userId: string, includePermissions?: boolean) => + ns.getAssignedRoles('oidc', userId, includePermissions), assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, 'oidc'), revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, 'oidc'), }; -} +}; /** Internal interface for operations that MAY accept a 'db'/'oidc' namespace. */ interface NamespacedUsers { - getAssignedRoles: (userType: UserTypeInternal, userId: string, includePermissions?: boolean) => Promise>; + getAssignedRoles: ( + userType: UserTypeInternal, + userId: string, + includePermissions?: boolean + ) => Promise>; assignRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => Promise; revokeRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => Promise; } @@ -140,19 +157,19 @@ interface NamespacedUsers { const baseUsers = (connection: ConnectionREST): UsersBase => { const ns = namespacedUsers(connection); return { - assignRoles: (roleNames: string | string[], userId: string) => - ns.assignRoles(roleNames, userId), - revokeRoles: (roleNames: string | string[], userId: string) => - ns.revokeRoles(roleNames, userId), + assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId), + revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId), }; -} +}; const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { return { getAssignedRoles: (userType: UserTypeInternal, userId: string, includePermissions?: boolean) => - connection.get( - `/authz/users/${userId}/roles/${userType}${includePermissions ? '?&includeFullRoles=true' : ''}` - ).then(Map.roles), + connection + .get( + `/authz/users/${userId}/roles/${userType}${includePermissions ? '?&includeFullRoles=true' : ''}` + ) + .then(Map.roles), assignRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => connection.postEmpty(`/authz/users/${userId}/assign`, { roles: Array.isArray(roleNames) ? roleNames : [roleNames], @@ -166,5 +183,4 @@ const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { }; }; - export default users; diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 83d2ec4a..56c8cbb8 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -1,11 +1,19 @@ import weaviate, { ApiKey } from '..'; import { DbVersion } from '../utils/dbVersion'; +import { WeaviateUserTypeDB } from '../v2'; +import { UserDB } from './types.js'; -const only = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0) - ? describe - : describe.skip; +const version = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`); -only('Integration testing of the users namespace', () => { +/** Run the suite / test only for Weaviate version above this. */ +const requireAtLeast = (...semver: [...Parameters]) => + version.isAtLeast(...semver) ? describe : describe.skip; + +requireAtLeast( + 1, + 29, + 0 +)('Integration testing of the users namespace', () => { const makeClient = (key: string) => weaviate.connectToLocal({ port: 8091, @@ -59,5 +67,93 @@ only('Integration testing of the users namespace', () => { expect(roles.test).toBeUndefined(); }); + requireAtLeast( + 1, + 30, + 0 + )('dynamic user management', () => { + it('should be able to manage "db" user lifecycle', async () => { + const client = await makeClient('admin-key'); + + /** Pass false to expect a rejected promise, chain assertions about dynamic-dave otherwise. */ + const expectDave = (ok: boolean = true) => { + const promise = expect(client.users.db.byName('dynamic-dave')); + return ok ? promise.resolves : promise.rejects; + }; + + await client.users.db.create('dynamic-dave'); + expectDave().toHaveProperty('active', true); + + // Second activation is a no-op + expect(client.users.db.activate('dynamic-dave')).resolves.toEqual(true); + + await client.users.db.deactivate('dynamic-dave'); + expectDave().toHaveProperty('active', false); + + // Second deactivation is a no-op + expect(client.users.db.deactivate('dynamic-dave')).resolves.toEqual(true); + + await client.users.db.delete('dynamic-dave'); + expectDave(false).toHaveProperty('code', 404); + }); + + it('should be able to obtain and rotate api keys', async () => { + const admin = await makeClient('admin-key'); + const apiKey = await admin.users.db.create('api-ashley'); + + let userAshley = await makeClient(apiKey).then((client) => client.users.getMyUser()); + expect(userAshley.id).toEqual('api-ashley'); + + const newKey = await admin.users.db.rotateKey('api-ashley'); + userAshley = await makeClient(newKey).then((client) => client.users.getMyUser()); + expect(userAshley.id).toEqual('api-ashley'); + }); + + it('should be able to list all dynamic users', async () => { + const admin = await makeClient('admin-key'); + + const created: Promise[] = []; + for (const user of ['jim', 'pam', 'dwight']) { + created.push(admin.users.db.create(user)); + } + await Promise.all(created); + + const all = await admin.users.db.listAll(); + expect(all.length).toBeGreaterThanOrEqual(3); + + const pam = await admin.users.db.byName('pam'); + expect(all).toEqual(expect.arrayContaining([pam])); + }); + + it('should be able to fetch static users', async () => { + const custom = await makeClient('admin-key').then((client) => client.users.db.byName('custom-user')); + expect(custom.userType).toEqual('db_env_user'); + }); + + it.each<'db' | 'oidc'>(['db', 'oidc'])('should be able to assign roles to "%s" users', async (kind) => { + const admin = await makeClient('admin-key'); + + if (kind === 'db') { + await admin.users.db.create('role-rick'); + } + + await admin.users.db.assignRoles('test', 'role-rick'); + expect(admin.users.db.getAssignedRoles('role-rick')).resolves.toEqual( + expect.objectContaining({ test: expect.any(Object) }) + ); + + await admin.users.db.revokeRoles('test', 'role-rick'); + expect(admin.users.db.getAssignedRoles('role-rick')).resolves.toEqual({}); + }); + + afterAll(() => + makeClient('admin-key').then((c) => { + for (const user of ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick']) { + c.users.db.delete(user); + } + }) + ); + }); + afterAll(() => makeClient('admin-key').then((c) => c.roles.delete('test'))); }); diff --git a/src/users/types.ts b/src/users/types.ts index a14dce06..01f4c85b 100644 --- a/src/users/types.ts +++ b/src/users/types.ts @@ -1,5 +1,5 @@ -import { Role } from '../roles/types.js'; import { WeaviateUserTypeDB as UserTypeDB } from '../openapi/types.js'; +import { Role } from '../roles/types.js'; export type User = { id: string; @@ -12,5 +12,3 @@ export type UserDB = { roleNames: string[]; active: boolean; }; - - From b1ece85707ed42060f9261c0137e88017be95594 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Mon, 31 Mar 2025 21:05:03 +0200 Subject: [PATCH 29/49] refactor(test): use concise Promise.all --- src/users/integration.test.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 56c8cbb8..02338f42 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -112,11 +112,7 @@ requireAtLeast( it('should be able to list all dynamic users', async () => { const admin = await makeClient('admin-key'); - const created: Promise[] = []; - for (const user of ['jim', 'pam', 'dwight']) { - created.push(admin.users.db.create(user)); - } - await Promise.all(created); + await Promise.all(['jim', 'pam', 'dwight'].map((user) => admin.users.db.create(user))); const all = await admin.users.db.listAll(); expect(all.length).toBeGreaterThanOrEqual(3); @@ -147,10 +143,10 @@ requireAtLeast( }); afterAll(() => - makeClient('admin-key').then((c) => { - for (const user of ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick']) { - c.users.db.delete(user); - } + makeClient('admin-key').then(async (c) => { + await Promise.all( + ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick'].map((n) => c.roles.delete(n)) + ); }) ); }); From d61c30fbf3ab0ea18225d86f734ba6287d6f7fdd Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Mon, 31 Mar 2025 21:47:21 +0200 Subject: [PATCH 30/49] feat(breaking): include user types in user assignments --- src/openapi/types.ts | 13 ++++++----- src/roles/index.ts | 14 ++++++++--- src/roles/integration.test.ts | 44 ++++++++++++++++++++++++++++++----- src/roles/types.ts | 7 +++++- src/roles/util.ts | 11 +++++++++ src/users/integration.test.ts | 8 +------ test/version.ts | 7 ++++++ 7 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 test/version.ts diff --git a/src/openapi/types.ts b/src/openapi/types.ts index 4924fc90..59e6e7d1 100644 --- a/src/openapi/types.ts +++ b/src/openapi/types.ts @@ -1,4 +1,4 @@ -import { definitions } from './schema.js'; +import { definitions, operations } from './schema.js'; type Override = Omit & T2; type DefaultProperties = { [key: string]: unknown }; @@ -54,11 +54,6 @@ export type WeaviateMultiTenancyConfig = WeaviateClass['multiTenancyConfig']; export type WeaviateReplicationConfig = WeaviateClass['replicationConfig']; export type WeaviateShardingConfig = WeaviateClass['shardingConfig']; export type WeaviateShardStatus = definitions['ShardStatusGetResponse']; -export type WeaviateUser = definitions['UserOwnInfo']; -export type WeaviateDBUser = definitions['DBUserInfo']; -export type WeaviateUserType = definitions['UserTypeOutput']; -export type WeaviateUserTypeInternal = definitions['UserTypeInput']; -export type WeaviateUserTypeDB = definitions['DBUserInfo']['dbUserType']; export type WeaviateVectorIndexConfig = WeaviateClass['vectorIndexConfig']; export type WeaviateVectorsConfig = WeaviateClass['vectorConfig']; export type WeaviateVectorConfig = definitions['VectorConfig']; @@ -73,3 +68,9 @@ export type Meta = definitions['Meta']; export type Role = definitions['Role']; export type Permission = definitions['Permission']; export type Action = definitions['Permission']['action']; +export type WeaviateUser = definitions['UserOwnInfo']; +export type WeaviateDBUser = definitions['DBUserInfo']; +export type WeaviateUserType = definitions['UserTypeOutput']; +export type WeaviateUserTypeInternal = definitions['UserTypeInput']; +export type WeaviateUserTypeDB = definitions['DBUserInfo']['dbUserType']; +export type WeaviateAssignedUser = operations['getUsersForRole']['responses']['200']['schema'][0]; diff --git a/src/roles/index.ts b/src/roles/index.ts index e5dd42fa..139e9dee 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -1,5 +1,9 @@ import { ConnectionREST } from '../index.js'; -import { Permission as WeaviatePermission, Role as WeaviateRole } from '../openapi/types.js'; +import { + WeaviateAssignedUser, + Permission as WeaviatePermission, + Role as WeaviateRole, +} from '../openapi/types.js'; import { BackupsPermission, ClusterPermission, @@ -11,6 +15,7 @@ import { Role, RolesPermission, TenantsPermission, + UserAssignment, UsersPermission, } from './types.js'; import { Map } from './util.js'; @@ -35,7 +40,7 @@ export interface Roles { * @param {string} roleName The name of the role to retrieve the assigned user IDs for. * @returns {Promise} The user IDs assigned to the role. */ - assignedUserIds: (roleName: string) => Promise; + userAssignments: (roleName: string) => Promise; /** * Delete a role by its name. * @@ -89,7 +94,10 @@ const roles = (connection: ConnectionREST): Roles => { listAll: () => connection.get('/authz/roles').then(Map.roles), byName: (roleName: string) => connection.get(`/authz/roles/${roleName}`).then(Map.roleFromWeaviate), - assignedUserIds: (roleName: string) => connection.get(`/authz/roles/${roleName}/users`), + userAssignments: (roleName: string) => + connection + .get(`/authz/roles/${roleName}/user-assignments`, true) + .then(Map.assignedUsers), create: (roleName: string, permissions?: PermissionsInput) => { const perms = permissions ? Map.flattenPermissions(permissions).flatMap(Map.permissionToWeaviate) diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index f90f0465..a2b6456d 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -7,9 +7,10 @@ import weaviate, { RolesAction, TenantsAction, WeaviateClient, + UserAssignment, } from '..'; +import { requireAtLeast } from '../../test/version'; import { WeaviateStartUpError, WeaviateUnexpectedStatusCodeError } from '../errors'; -import { DbVersion } from '../utils/dbVersion'; type TestCase = { roleName: string; @@ -278,11 +279,11 @@ const testCases: TestCase[] = [ }, ]; -const maybe = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0) - ? describe - : describe.skip; - -maybe('Integration testing of the roles namespace', () => { +requireAtLeast( + 1, + 29, + 0 +)('Integration testing of the roles namespace', () => { let client: WeaviateClient; beforeAll(async () => { @@ -316,6 +317,37 @@ maybe('Integration testing of the roles namespace', () => { expect(exists).toBeFalsy(); }); + requireAtLeast( + 1, + 30, + 0 + )('namespaced users', () => { + it('retrieves assigned users with namespace', async () => { + await client.roles.create('landlord', { + collection: 'Buildings', + tenant: 'john doe', + actions: ['create_tenants', 'delete_tenants'], + }); + + await client.users.db.create('Innkeeper').catch((res) => expect(res.code).toEqual(409)); + + await client.users.db.assignRoles('landlord', 'custom-user'); + await client.users.db.assignRoles('landlord', 'Innkeeper'); + + const assignments = await client.roles.userAssignments('landlord'); + + expect(assignments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'custom-user', userType: 'db_env_user' }), + expect.objectContaining({ id: 'Innkeeper', userType: 'db_user' }), + ]) + ); + + await client.users.db.delete('Innkeeper'); + await client.roles.delete('landlord'); + }); + }); + describe('should be able to create roles using the permissions factory', () => { testCases.forEach((testCase) => { it(`with ${testCase.roleName} permissions`, async () => { diff --git a/src/roles/types.ts b/src/roles/types.ts index 3a6d0266..93273521 100644 --- a/src/roles/types.ts +++ b/src/roles/types.ts @@ -1,4 +1,4 @@ -import { Action } from '../openapi/types.js'; +import { Action, WeaviateUserType } from '../openapi/types.js'; export type BackupsAction = Extract; export type ClusterAction = Extract; @@ -22,6 +22,11 @@ export type TenantsAction = Extract< >; export type UsersAction = Extract; +export type UserAssignment = { + id: string; + userType: WeaviateUserType; +}; + export type BackupsPermission = { collection: string; actions: BackupsAction[]; diff --git a/src/roles/util.ts b/src/roles/util.ts index b6ff39d2..53571437 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -1,4 +1,5 @@ import { + WeaviateAssignedUser, WeaviateDBUser, Permission as WeaviatePermission, Role as WeaviateRole, @@ -23,6 +24,7 @@ import { RolesPermission, TenantsAction, TenantsPermission, + UserAssignment, UsersAction, UsersPermission, } from './types.js'; @@ -153,6 +155,15 @@ export class Map { acc.push(Map.dbUser(user)); return acc; }, [] as UserDB[]); + + static assignedUsers = (users: WeaviateAssignedUser[]): UserAssignment[] => + users.reduce((acc, user) => { + acc.push({ + id: user.userId || '', + userType: user.userType, + }); + return acc; + }, [] as UserAssignment[]); } class PermissionsMapping { diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 02338f42..305bc371 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -1,14 +1,8 @@ import weaviate, { ApiKey } from '..'; -import { DbVersion } from '../utils/dbVersion'; +import { requireAtLeast } from '../../test/version.js'; import { WeaviateUserTypeDB } from '../v2'; import { UserDB } from './types.js'; -const version = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`); - -/** Run the suite / test only for Weaviate version above this. */ -const requireAtLeast = (...semver: [...Parameters]) => - version.isAtLeast(...semver) ? describe : describe.skip; - requireAtLeast( 1, 29, diff --git a/test/version.ts b/test/version.ts new file mode 100644 index 00000000..b34118ef --- /dev/null +++ b/test/version.ts @@ -0,0 +1,7 @@ +import { DbVersion } from '../src/utils/dbVersion'; + +const version = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`); + +/** Run the suite / test only for Weaviate version above this. */ +export const requireAtLeast = (...semver: [...Parameters]) => + version.isAtLeast(...semver) ? describe : describe.skip; From 795fdb7a125bd9983afa152198299bbb67c982b2 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 1 Apr 2025 13:42:32 +0200 Subject: [PATCH 31/49] chore: format and lint --- src/roles/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index a2b6456d..1d1b24b2 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -6,8 +6,8 @@ import weaviate, { Role, RolesAction, TenantsAction, - WeaviateClient, UserAssignment, + WeaviateClient, } from '..'; import { requireAtLeast } from '../../test/version'; import { WeaviateStartUpError, WeaviateUnexpectedStatusCodeError } from '../errors'; From 5562ec99c05062b5fdf8be95fd9067e61ddbd1b9 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 1 Apr 2025 13:50:26 +0200 Subject: [PATCH 32/49] test: activate test case for 'oidc' users --- src/users/integration.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 305bc371..453d97dc 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -127,19 +127,19 @@ requireAtLeast( await admin.users.db.create('role-rick'); } - await admin.users.db.assignRoles('test', 'role-rick'); - expect(admin.users.db.getAssignedRoles('role-rick')).resolves.toEqual( + await admin.users[kind].assignRoles('test', 'role-rick'); + expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual( expect.objectContaining({ test: expect.any(Object) }) ); - await admin.users.db.revokeRoles('test', 'role-rick'); - expect(admin.users.db.getAssignedRoles('role-rick')).resolves.toEqual({}); + await admin.users[kind].revokeRoles('test', 'role-rick'); + expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual({}) }); afterAll(() => makeClient('admin-key').then(async (c) => { await Promise.all( - ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick'].map((n) => c.roles.delete(n)) + ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick'].map(n => c.users.db.delete(n)) ); }) ); From 6733882629108c2ca04d99e2d7f88fc4cd3501ce Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 1 Apr 2025 13:54:37 +0200 Subject: [PATCH 33/49] chore: lint and format --- src/users/integration.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 453d97dc..493de984 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -133,13 +133,13 @@ requireAtLeast( ); await admin.users[kind].revokeRoles('test', 'role-rick'); - expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual({}) + expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual({}); }); afterAll(() => makeClient('admin-key').then(async (c) => { await Promise.all( - ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick'].map(n => c.users.db.delete(n)) + ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick'].map((n) => c.users.db.delete(n)) ); }) ); From df010a2e5cc2f6538d65b75d33d3be8debb63398 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 1 Apr 2025 17:34:03 +0200 Subject: [PATCH 34/49] refactor: replace .reduce with .map where possible Rewrote other .reduce occurences in a more succinct manner --- src/roles/util.ts | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/roles/util.ts b/src/roles/util.ts index 53571437..5ccd427f 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -130,16 +130,16 @@ export class Map { static roleFromWeaviate = (role: WeaviateRole): Role => PermissionsMapping.use(role).map(); static roles = (roles: WeaviateRole[]): Record => - roles.reduce((acc, role) => { - acc[role.name] = Map.roleFromWeaviate(role); - return acc; - }, {} as Record); + roles.reduce((acc, role) => ({ + ...acc, + [role.name]: Map.roleFromWeaviate(role), + }), {} as Record); static users = (users: string[]): Record => - users.reduce((acc, user) => { - acc[user] = { id: user }; - return acc; - }, {} as Record); + users.reduce((acc, user) => ({ + ...acc, + [user]: { id: user }, + }), {} as Record); static user = (user: WeaviateUser): User => ({ id: user.username, roles: user.roles?.map(Map.roleFromWeaviate), @@ -151,19 +151,12 @@ export class Map { active: user.active, }); static dbUsers = (users: WeaviateDBUser[]): UserDB[] => - users.reduce((acc, user) => { - acc.push(Map.dbUser(user)); - return acc; - }, [] as UserDB[]); - + users.map(Map.dbUser); static assignedUsers = (users: WeaviateAssignedUser[]): UserAssignment[] => - users.reduce((acc, user) => { - acc.push({ - id: user.userId || '', - userType: user.userType, - }); - return acc; - }, [] as UserAssignment[]); + users.map((user) => ({ + id: user.userId || '', + userType: user.userType, + })); } class PermissionsMapping { From dffc2c128e163174863f4dd303aa2f754eb12f7d Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 1 Apr 2025 17:44:36 +0200 Subject: [PATCH 35/49] test: use valid tenant name --- src/roles/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index 1d1b24b2..2035ef05 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -325,7 +325,7 @@ requireAtLeast( it('retrieves assigned users with namespace', async () => { await client.roles.create('landlord', { collection: 'Buildings', - tenant: 'john doe', + tenant: 'john-doe', actions: ['create_tenants', 'delete_tenants'], }); From 29f3cfcfb5672ae307b2d730d237ddedbe840416 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 1 Apr 2025 17:45:09 +0200 Subject: [PATCH 36/49] refactor: collect optional parameters in an object --- src/users/index.ts | 48 +++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index 5e2b9a35..f237b571 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -59,7 +59,7 @@ export interface DBUsers extends UsersBase { * @param {string} userId The ID of the user to retrieve the assigned roles for. * @returns {Promise>} A map of role names to their respective roles. */ - getAssignedRoles: (userId: string, includePermissions?: boolean) => Promise>; + getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => Promise>; create: (userId: string) => Promise; delete: (userId: string) => Promise; @@ -70,6 +70,10 @@ export interface DBUsers extends UsersBase { listAll: () => Promise; } +type GetAssingedRolesOptions = { + includePermissions?: boolean; +} + /** Operations supported for namespaced 'oidc' users.*/ export interface OIDCUsers extends UsersBase { /** @@ -78,7 +82,7 @@ export interface OIDCUsers extends UsersBase { * @param {string} userId The ID of the user to retrieve the assigned roles for. * @returns {Promise>} A map of role names to their respective roles. */ - getAssignedRoles: (userId: string, includePermissions?: boolean) => Promise>; + getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => Promise>; } const users = (connection: ConnectionREST): Users => { @@ -104,10 +108,10 @@ const db = (connection: ConnectionREST): DBUsers => { type APIKeyResponse = { apikey: string }; return { - getAssignedRoles: (userId: string, includePermissions?: boolean) => - ns.getAssignedRoles('db', userId, includePermissions), - assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, 'db'), - revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, 'db'), + getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => + ns.getAssignedRoles('db', userId, opts), + assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, { userType: 'db' }), + revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, { userType: 'db' }), create: (userId: string) => connection.postNoBody(`/users/db/${userId}`).then((resp) => resp.apikey), @@ -136,22 +140,19 @@ const db = (connection: ConnectionREST): DBUsers => { const oidc = (connection: ConnectionREST): OIDCUsers => { const ns = namespacedUsers(connection); return { - getAssignedRoles: (userId: string, includePermissions?: boolean) => - ns.getAssignedRoles('oidc', userId, includePermissions), - assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, 'oidc'), - revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, 'oidc'), + getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => + ns.getAssignedRoles('oidc', userId, opts), + assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, { userType: 'oidc' }), + revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, { userType: 'oidc' }), }; }; /** Internal interface for operations that MAY accept a 'db'/'oidc' namespace. */ interface NamespacedUsers { - getAssignedRoles: ( - userType: UserTypeInternal, - userId: string, - includePermissions?: boolean + getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssingedRolesOptions ) => Promise>; - assignRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => Promise; - revokeRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => Promise; + assignRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => Promise; + revokeRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => Promise; } const baseUsers = (connection: ConnectionREST): UsersBase => { @@ -162,23 +163,26 @@ const baseUsers = (connection: ConnectionREST): UsersBase => { }; }; +/** Optional arguments to /assign and /revoke endpoints. */ +type AssignRevokeOptions = { userType?: UserTypeInternal } + const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { return { - getAssignedRoles: (userType: UserTypeInternal, userId: string, includePermissions?: boolean) => + getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssingedRolesOptions) => connection .get( - `/authz/users/${userId}/roles/${userType}${includePermissions ? '?&includeFullRoles=true' : ''}` + `/authz/users/${userId}/roles/${userType}${opts?.includePermissions ? '?&includeFullRoles=true' : ''}` ) .then(Map.roles), - assignRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => + assignRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => connection.postEmpty(`/authz/users/${userId}/assign`, { + ...opts, roles: Array.isArray(roleNames) ? roleNames : [roleNames], - userType: userType, }), - revokeRoles: (roleNames: string | string[], userId: string, userType?: UserTypeInternal) => + revokeRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => connection.postEmpty(`/authz/users/${userId}/revoke`, { + ...opts, roles: Array.isArray(roleNames) ? roleNames : [roleNames], - userType: userType, }), }; }; From d32ac420b54e7203db676fab8f726379cbd46c34 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 1 Apr 2025 18:22:17 +0200 Subject: [PATCH 37/49] test: add test case w/ includePermissions=true --- src/users/integration.test.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 493de984..21a8ab05 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -1,4 +1,4 @@ -import weaviate, { ApiKey } from '..'; +import weaviate, { ApiKey, Role } from '..'; import { requireAtLeast } from '../../test/version.js'; import { WeaviateUserTypeDB } from '../v2'; import { UserDB } from './types.js'; @@ -136,10 +136,29 @@ requireAtLeast( expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual({}); }); + it('should be able to fetch assigned roles with all permissions', async () => { + const admin = await makeClient('admin-key'); + + await admin.roles.delete('test'); + await admin.roles.create('test', [ + { collection: 'Things', actions: ['manage_backups'] }, + { collection: 'Things', tenant: 'data-tenant', actions: ['create_data'] }, + { collection: 'Things', verbosity: 'minimal', actions: ['read_nodes'] }, + ]); + await admin.users.db.create('permission-peter'); + await admin.users.db.assignRoles('test', 'permission-peter'); + + const roles = await admin.users.db.getAssignedRoles('permission-peter', { includePermissions: true }); + expect(roles['test'].backupsPermissions).toHaveLength(1); + expect(roles['test'].dataPermissions).toHaveLength(1); + expect(roles['test'].nodesPermissions).toHaveLength(1); + + }); + afterAll(() => makeClient('admin-key').then(async (c) => { await Promise.all( - ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick'].map((n) => c.users.db.delete(n)) + ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick', 'permission-peter'].map((n) => c.users.db.delete(n)) ); }) ); From 5bc5b08f8c19d2a1d09a985f70e17045bcfbe515 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 1 Apr 2025 18:24:20 +0200 Subject: [PATCH 38/49] chore: lint and format --- src/roles/util.ts | 25 +++++++++++++++---------- src/users/index.ts | 25 +++++++++++++++++-------- src/users/integration.test.ts | 13 +++++++------ 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/roles/util.ts b/src/roles/util.ts index 5ccd427f..09f82bb0 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -130,16 +130,22 @@ export class Map { static roleFromWeaviate = (role: WeaviateRole): Role => PermissionsMapping.use(role).map(); static roles = (roles: WeaviateRole[]): Record => - roles.reduce((acc, role) => ({ - ...acc, - [role.name]: Map.roleFromWeaviate(role), - }), {} as Record); + roles.reduce( + (acc, role) => ({ + ...acc, + [role.name]: Map.roleFromWeaviate(role), + }), + {} as Record + ); static users = (users: string[]): Record => - users.reduce((acc, user) => ({ - ...acc, - [user]: { id: user }, - }), {} as Record); + users.reduce( + (acc, user) => ({ + ...acc, + [user]: { id: user }, + }), + {} as Record + ); static user = (user: WeaviateUser): User => ({ id: user.username, roles: user.roles?.map(Map.roleFromWeaviate), @@ -150,8 +156,7 @@ export class Map { roleNames: user.roles, active: user.active, }); - static dbUsers = (users: WeaviateDBUser[]): UserDB[] => - users.map(Map.dbUser); + static dbUsers = (users: WeaviateDBUser[]): UserDB[] => users.map(Map.dbUser); static assignedUsers = (users: WeaviateAssignedUser[]): UserAssignment[] => users.map((user) => ({ id: user.userId || '', diff --git a/src/users/index.ts b/src/users/index.ts index f237b571..27f65fce 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -72,7 +72,7 @@ export interface DBUsers extends UsersBase { type GetAssingedRolesOptions = { includePermissions?: boolean; -} +}; /** Operations supported for namespaced 'oidc' users.*/ export interface OIDCUsers extends UsersBase { @@ -110,8 +110,10 @@ const db = (connection: ConnectionREST): DBUsers => { return { getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => ns.getAssignedRoles('db', userId, opts), - assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, { userType: 'db' }), - revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, { userType: 'db' }), + assignRoles: (roleNames: string | string[], userId: string) => + ns.assignRoles(roleNames, userId, { userType: 'db' }), + revokeRoles: (roleNames: string | string[], userId: string) => + ns.revokeRoles(roleNames, userId, { userType: 'db' }), create: (userId: string) => connection.postNoBody(`/users/db/${userId}`).then((resp) => resp.apikey), @@ -142,14 +144,19 @@ const oidc = (connection: ConnectionREST): OIDCUsers => { return { getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => ns.getAssignedRoles('oidc', userId, opts), - assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, { userType: 'oidc' }), - revokeRoles: (roleNames: string | string[], userId: string) => ns.revokeRoles(roleNames, userId, { userType: 'oidc' }), + assignRoles: (roleNames: string | string[], userId: string) => + ns.assignRoles(roleNames, userId, { userType: 'oidc' }), + revokeRoles: (roleNames: string | string[], userId: string) => + ns.revokeRoles(roleNames, userId, { userType: 'oidc' }), }; }; /** Internal interface for operations that MAY accept a 'db'/'oidc' namespace. */ interface NamespacedUsers { - getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssingedRolesOptions + getAssignedRoles: ( + userType: UserTypeInternal, + userId: string, + opts?: GetAssingedRolesOptions ) => Promise>; assignRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => Promise; revokeRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => Promise; @@ -164,14 +171,16 @@ const baseUsers = (connection: ConnectionREST): UsersBase => { }; /** Optional arguments to /assign and /revoke endpoints. */ -type AssignRevokeOptions = { userType?: UserTypeInternal } +type AssignRevokeOptions = { userType?: UserTypeInternal }; const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { return { getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssingedRolesOptions) => connection .get( - `/authz/users/${userId}/roles/${userType}${opts?.includePermissions ? '?&includeFullRoles=true' : ''}` + `/authz/users/${userId}/roles/${userType}${ + opts?.includePermissions ? '?&includeFullRoles=true' : '' + }` ) .then(Map.roles), assignRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 21a8ab05..8abb1d88 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -1,4 +1,4 @@ -import weaviate, { ApiKey, Role } from '..'; +import weaviate, { ApiKey } from '..'; import { requireAtLeast } from '../../test/version.js'; import { WeaviateUserTypeDB } from '../v2'; import { UserDB } from './types.js'; @@ -149,16 +149,17 @@ requireAtLeast( await admin.users.db.assignRoles('test', 'permission-peter'); const roles = await admin.users.db.getAssignedRoles('permission-peter', { includePermissions: true }); - expect(roles['test'].backupsPermissions).toHaveLength(1); - expect(roles['test'].dataPermissions).toHaveLength(1); - expect(roles['test'].nodesPermissions).toHaveLength(1); - + expect(roles.test.backupsPermissions).toHaveLength(1); + expect(roles.test.dataPermissions).toHaveLength(1); + expect(roles.test.nodesPermissions).toHaveLength(1); }); afterAll(() => makeClient('admin-key').then(async (c) => { await Promise.all( - ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick', 'permission-peter'].map((n) => c.users.db.delete(n)) + ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick', 'permission-peter'].map((n) => + c.users.db.delete(n) + ) ); }) ); From ef70fc2c973583e27a3f14b39a2992f1464d4b7d Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Wed, 2 Apr 2025 11:34:54 +0100 Subject: [PATCH 39/49] Add missing modules and module params for - `baseURL` in `generative-anthropic` - `videoIntervalSeconds` in `multi2vec-google` - `outputEncoding` in `Multi2VecVoyageAIConfig` - the entire `text2vec-nvidia` module --- src/collections/config/types/generative.ts | 1 + src/collections/config/types/vectorizer.ts | 23 +++++++++++ src/collections/configure/types/vectorizer.ts | 5 +++ src/collections/configure/unit.test.ts | 41 +++++++++++++++++++ src/collections/configure/vectorizer.ts | 13 ++++++ 5 files changed, 83 insertions(+) diff --git a/src/collections/config/types/generative.ts b/src/collections/config/types/generative.ts index 7ff426b5..d275f201 100644 --- a/src/collections/config/types/generative.ts +++ b/src/collections/config/types/generative.ts @@ -15,6 +15,7 @@ export type GenerativeAWSConfig = { }; export type GenerativeAnthropicConfig = { + baseURL?: string; maxTokens?: number; model?: string; stopSequences?: string[]; diff --git a/src/collections/config/types/vectorizer.ts b/src/collections/config/types/vectorizer.ts index 475c941d..99dd41f8 100644 --- a/src/collections/config/types/vectorizer.ts +++ b/src/collections/config/types/vectorizer.ts @@ -35,6 +35,7 @@ export type Vectorizer = | 'text2vec-gpt4all' | 'text2vec-huggingface' | 'text2vec-jinaai' + | 'text2vec-nvidia' | 'text2vec-mistral' | 'text2vec-ollama' | 'text2vec-openai' @@ -169,6 +170,8 @@ export type Multi2VecGoogleConfig = { textFields?: string[]; /** The video fields used when vectorizing. */ videoFields?: string[]; + /** Length of a video interval in seconds. */ + videoIntervalSeconds?: number; /** The model ID in use. */ modelId?: string; /** The dimensionality of the vector once embedded. */ @@ -223,6 +226,8 @@ export type Multi2VecVoyageAIConfig = { imageFields?: string[]; /** The model to use. */ model?: string; + /** How the output from the model should be encoded on return. */ + outputEncoding?: string; /** The text fields used when vectorizing. */ textFields?: string[]; /** Whether the input should be truncated to fit in the context window. */ @@ -363,6 +368,22 @@ export type Text2VecJinaAIConfig = { /** @deprecated Use `Text2VecJinaAIConfig` instead. */ export type Text2VecJinaConfig = Text2VecJinaAIConfig; +/** + * The configuration for text vectorization using the Nvidia module. + * + * See the [documentation](https://weaviate.io/developers/weaviate/model-providers/nvidia/embeddings) for detailed usage. + */ +export type Text2VecNvidiaConfig = { + /** The base URL to use where API requests should go. */ + baseURL?: string; + /** The model to use. */ + model?: string; + /** Whether to truncate when vectorising. */ + truncate?: boolean; + /** Whether to vectorize the collection name. */ + vectorizeCollectionName?: boolean; +}; + /** * The configuration for text vectorization using the Mistral module. * @@ -541,6 +562,8 @@ export type VectorizerConfigType = V extends 'img2vec-neural' ? Text2VecHuggingFaceConfig | undefined : V extends 'text2vec-jinaai' ? Text2VecJinaAIConfig | undefined + : V extends 'text2vec-nvidia' + ? Text2VecNvidiaConfig | undefined : V extends 'text2vec-mistral' ? Text2VecMistralConfig | undefined : V extends 'text2vec-ollama' diff --git a/src/collections/configure/types/vectorizer.ts b/src/collections/configure/types/vectorizer.ts index 5318e8f4..5391c356 100644 --- a/src/collections/configure/types/vectorizer.ts +++ b/src/collections/configure/types/vectorizer.ts @@ -13,6 +13,7 @@ import { Text2VecHuggingFaceConfig, Text2VecJinaAIConfig, Text2VecMistralConfig, + Text2VecNvidiaConfig, Text2VecOllamaConfig, Text2VecOpenAIConfig, Text2VecTransformersConfig, @@ -198,6 +199,8 @@ export type Text2VecHuggingFaceConfigCreate = Text2VecHuggingFaceConfig; export type Text2VecJinaAIConfigCreate = Text2VecJinaAIConfig; +export type Text2VecNvidiaConfigCreate = Text2VecNvidiaConfig; + export type Text2VecMistralConfigCreate = Text2VecMistralConfig; export type Text2VecOllamaConfigCreate = Text2VecOllamaConfig; @@ -247,6 +250,8 @@ export type VectorizerConfigCreateType = V extends 'img2vec-neural' ? Text2VecHuggingFaceConfigCreate | undefined : V extends 'text2vec-jinaai' ? Text2VecJinaAIConfigCreate | undefined + : V extends 'text2vec-nvidia' + ? Text2VecNvidiaConfigCreate | undefined : V extends 'text2vec-mistral' ? Text2VecMistralConfigCreate | undefined : V extends 'text2vec-ollama' diff --git a/src/collections/configure/unit.test.ts b/src/collections/configure/unit.test.ts index 93c50f65..36a9ce6b 100644 --- a/src/collections/configure/unit.test.ts +++ b/src/collections/configure/unit.test.ts @@ -1161,6 +1161,47 @@ describe('Unit testing of the vectorizer factory class', () => { }); }); + it('should create the correct Text2VecNvidiaConfig type with defaults', () => { + const config = configure.vectorizer.text2VecNvidia(); + expect(config).toEqual>({ + name: undefined, + vectorIndex: { + name: 'hnsw', + config: undefined, + }, + vectorizer: { + name: 'text2vec-nvidia', + config: undefined, + }, + }); + }); + + it('should create the correct Text2VecNvidiaConfig type with all values', () => { + const config = configure.vectorizer.text2VecNvidia({ + name: 'test', + baseURL: 'base-url', + model: 'model', + truncate: true, + vectorizeCollectionName: true, + }); + expect(config).toEqual>({ + name: 'test', + vectorIndex: { + name: 'hnsw', + config: undefined, + }, + vectorizer: { + name: 'text2vec-nvidia', + config: { + baseURL: 'base-url', + model: 'model', + truncate: true, + vectorizeCollectionName: true, + }, + }, + }); + }); + it('should create the correct Text2VecMistralConfig type with defaults', () => { const config = configure.vectorizer.text2VecMistral(); expect(config).toEqual>({ diff --git a/src/collections/configure/vectorizer.ts b/src/collections/configure/vectorizer.ts index c0442658..298b1f01 100644 --- a/src/collections/configure/vectorizer.ts +++ b/src/collections/configure/vectorizer.ts @@ -518,6 +518,19 @@ export const vectorizer = { }, }); }, + text2VecNvidia: ( + opts?: ConfigureTextVectorizerOptions + ): VectorConfigCreate, N, I, 'text2vec-nvidia'> => { + const { name, sourceProperties, vectorIndexConfig, ...config } = opts || {}; + return makeVectorizer(name, { + sourceProperties, + vectorIndexConfig, + vectorizerConfig: { + name: 'text2vec-nvidia', + config: Object.keys(config).length === 0 ? undefined : config, + }, + }); + }, /** * Create a `VectorConfigCreate` object with the vectorizer set to `'text2vec-mistral'`. * From 53049e33ade0b0209176db5eca57f721e52cda37 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Wed, 2 Apr 2025 16:50:43 +0200 Subject: [PATCH 40/49] fix: expect no response for /activate and /deactivate --- src/users/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index 27f65fce..2f99a55b 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -126,12 +126,12 @@ const db = (connection: ConnectionREST): DBUsers => { connection.postNoBody(`/users/db/${userId}/rotate-key`).then((resp) => resp.apikey), activate: (userId: string) => connection - .postNoBody(`/users/db/${userId}/activate`) + .postEmpty(`/users/db/${userId}/activate`, null) .then(() => true) .catch(allowCode(409)), deactivate: (userId: string) => connection - .postNoBody(`/users/db/${userId}/deactivate`) + .postEmpty(`/users/db/${userId}/deactivate`, null) .then(() => true) .catch(allowCode(409)), byName: (userId: string) => connection.get(`/users/db/${userId}`, true).then(Map.dbUser), @@ -178,8 +178,7 @@ const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssingedRolesOptions) => connection .get( - `/authz/users/${userId}/roles/${userType}${ - opts?.includePermissions ? '?&includeFullRoles=true' : '' + `/authz/users/${userId}/roles/${userType}${opts?.includePermissions ? '?&includeFullRoles=true' : '' }` ) .then(Map.roles), From c0ca1eff283dd9f4c31936e695ae2379f25b3ab6 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Wed, 2 Apr 2025 18:12:50 +0200 Subject: [PATCH 41/49] fix: allow expected code only for instances of WeaviateUnexpectedStatusCodeError --- src/users/index.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index 2f99a55b..cd9c9f0a 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -1,3 +1,4 @@ +import { WeaviateUnexpectedStatusCodeError } from '../errors.js'; import { ConnectionREST } from '../index.js'; import { WeaviateUserTypeInternal as UserTypeInternal, @@ -102,8 +103,14 @@ const users = (connection: ConnectionREST): Users => { const db = (connection: ConnectionREST): DBUsers => { const ns = namespacedUsers(connection); - const allowCode = (code: number): ((reason: any) => boolean) => { - return (reason) => reason.code !== undefined && reason.code === code; + /** expectCode returns true if the error contained an expected status code. */ + const expectCode = (code: number): ((_: any) => boolean) => { + return (error) => { + if (error instanceof WeaviateUnexpectedStatusCodeError) { + return error.code === code; + } + throw error; + } }; type APIKeyResponse = { apikey: string }; @@ -128,12 +135,12 @@ const db = (connection: ConnectionREST): DBUsers => { connection .postEmpty(`/users/db/${userId}/activate`, null) .then(() => true) - .catch(allowCode(409)), + .catch(expectCode(409)), deactivate: (userId: string) => connection .postEmpty(`/users/db/${userId}/deactivate`, null) .then(() => true) - .catch(allowCode(409)), + .catch(expectCode(409)), byName: (userId: string) => connection.get(`/users/db/${userId}`, true).then(Map.dbUser), listAll: () => connection.get('/users/db', true).then(Map.dbUsers), }; From 28c3897fcae33aeb6ff77b2fa157dd49e80042dc Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Wed, 2 Apr 2025 18:16:23 +0200 Subject: [PATCH 42/49] refactor: declare types in **/types.ts --- src/users/index.ts | 21 +++++++-------------- src/users/types.ts | 11 ++++++++++- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index cd9c9f0a..ae1725a1 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -8,7 +8,7 @@ import { } from '../openapi/types.js'; import { Role } from '../roles/types.js'; import { Map } from '../roles/util.js'; -import { User, UserDB } from './types.js'; +import { AssignRevokeOptions, GetAssignedRolesOptions, User, UserDB } from './types.js'; /** * Operations supported for 'db', 'oidc', and legacy (non-namespaced) users. @@ -60,7 +60,7 @@ export interface DBUsers extends UsersBase { * @param {string} userId The ID of the user to retrieve the assigned roles for. * @returns {Promise>} A map of role names to their respective roles. */ - getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => Promise>; + getAssignedRoles: (userId: string, opts?: GetAssignedRolesOptions) => Promise>; create: (userId: string) => Promise; delete: (userId: string) => Promise; @@ -71,10 +71,6 @@ export interface DBUsers extends UsersBase { listAll: () => Promise; } -type GetAssingedRolesOptions = { - includePermissions?: boolean; -}; - /** Operations supported for namespaced 'oidc' users.*/ export interface OIDCUsers extends UsersBase { /** @@ -83,7 +79,7 @@ export interface OIDCUsers extends UsersBase { * @param {string} userId The ID of the user to retrieve the assigned roles for. * @returns {Promise>} A map of role names to their respective roles. */ - getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => Promise>; + getAssignedRoles: (userId: string, opts?: GetAssignedRolesOptions) => Promise>; } const users = (connection: ConnectionREST): Users => { @@ -115,7 +111,7 @@ const db = (connection: ConnectionREST): DBUsers => { type APIKeyResponse = { apikey: string }; return { - getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => + getAssignedRoles: (userId: string, opts?: GetAssignedRolesOptions) => ns.getAssignedRoles('db', userId, opts), assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, { userType: 'db' }), @@ -149,7 +145,7 @@ const db = (connection: ConnectionREST): DBUsers => { const oidc = (connection: ConnectionREST): OIDCUsers => { const ns = namespacedUsers(connection); return { - getAssignedRoles: (userId: string, opts?: GetAssingedRolesOptions) => + getAssignedRoles: (userId: string, opts?: GetAssignedRolesOptions) => ns.getAssignedRoles('oidc', userId, opts), assignRoles: (roleNames: string | string[], userId: string) => ns.assignRoles(roleNames, userId, { userType: 'oidc' }), @@ -163,7 +159,7 @@ interface NamespacedUsers { getAssignedRoles: ( userType: UserTypeInternal, userId: string, - opts?: GetAssingedRolesOptions + opts?: GetAssignedRolesOptions ) => Promise>; assignRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => Promise; revokeRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => Promise; @@ -177,12 +173,9 @@ const baseUsers = (connection: ConnectionREST): UsersBase => { }; }; -/** Optional arguments to /assign and /revoke endpoints. */ -type AssignRevokeOptions = { userType?: UserTypeInternal }; - const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { return { - getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssingedRolesOptions) => + getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssignedRolesOptions) => connection .get( `/authz/users/${userId}/roles/${userType}${opts?.includePermissions ? '?&includeFullRoles=true' : '' diff --git a/src/users/types.ts b/src/users/types.ts index 01f4c85b..0d54ef6f 100644 --- a/src/users/types.ts +++ b/src/users/types.ts @@ -1,4 +1,4 @@ -import { WeaviateUserTypeDB as UserTypeDB } from '../openapi/types.js'; +import { WeaviateUserTypeDB as UserTypeDB, WeaviateUserTypeInternal } from '../openapi/types.js'; import { Role } from '../roles/types.js'; export type User = { @@ -12,3 +12,12 @@ export type UserDB = { roleNames: string[]; active: boolean; }; + +/** Optional arguments to /user/{type}/{username} enpoint. */ +export type GetAssignedRolesOptions = { + includePermissions?: boolean; +}; + +/** Optional arguments to /assign and /revoke endpoints. */ +export type AssignRevokeOptions = { userType?: WeaviateUserTypeInternal }; + From 84aac0d21c91c2afdd1bd7d2bfe9200856795535 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Wed, 2 Apr 2025 18:21:37 +0200 Subject: [PATCH 43/49] test: await on all expectations which should resolve/reject --- src/roles/integration.test.ts | 1 - src/users/index.ts | 5 +++-- src/users/integration.test.ts | 14 +++++++------- src/users/types.ts | 1 - 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index 2035ef05..1d335c9f 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -335,7 +335,6 @@ requireAtLeast( await client.users.db.assignRoles('landlord', 'Innkeeper'); const assignments = await client.roles.userAssignments('landlord'); - expect(assignments).toEqual( expect.arrayContaining([ expect.objectContaining({ id: 'custom-user', userType: 'db_env_user' }), diff --git a/src/users/index.ts b/src/users/index.ts index ae1725a1..29e0476c 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -106,7 +106,7 @@ const db = (connection: ConnectionREST): DBUsers => { return error.code === code; } throw error; - } + }; }; type APIKeyResponse = { apikey: string }; @@ -178,7 +178,8 @@ const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssignedRolesOptions) => connection .get( - `/authz/users/${userId}/roles/${userType}${opts?.includePermissions ? '?&includeFullRoles=true' : '' + `/authz/users/${userId}/roles/${userType}${ + opts?.includePermissions ? '?&includeFullRoles=true' : '' }` ) .then(Map.roles), diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 8abb1d88..91f7b453 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -76,19 +76,19 @@ requireAtLeast( }; await client.users.db.create('dynamic-dave'); - expectDave().toHaveProperty('active', true); + await expectDave().toHaveProperty('active', true); // Second activation is a no-op - expect(client.users.db.activate('dynamic-dave')).resolves.toEqual(true); + await expect(client.users.db.activate('dynamic-dave')).resolves.toEqual(true); await client.users.db.deactivate('dynamic-dave'); - expectDave().toHaveProperty('active', false); + await expectDave().toHaveProperty('active', false); // Second deactivation is a no-op - expect(client.users.db.deactivate('dynamic-dave')).resolves.toEqual(true); + await expect(client.users.db.deactivate('dynamic-dave')).resolves.toEqual(true); await client.users.db.delete('dynamic-dave'); - expectDave(false).toHaveProperty('code', 404); + await expectDave(false).toHaveProperty('code', 404); }); it('should be able to obtain and rotate api keys', async () => { @@ -128,12 +128,12 @@ requireAtLeast( } await admin.users[kind].assignRoles('test', 'role-rick'); - expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual( + await expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual( expect.objectContaining({ test: expect.any(Object) }) ); await admin.users[kind].revokeRoles('test', 'role-rick'); - expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual({}); + await expect(admin.users[kind].getAssignedRoles('role-rick')).resolves.toEqual({}); }); it('should be able to fetch assigned roles with all permissions', async () => { diff --git a/src/users/types.ts b/src/users/types.ts index 0d54ef6f..6dee0ad7 100644 --- a/src/users/types.ts +++ b/src/users/types.ts @@ -20,4 +20,3 @@ export type GetAssignedRolesOptions = { /** Optional arguments to /assign and /revoke endpoints. */ export type AssignRevokeOptions = { userType?: WeaviateUserTypeInternal }; - From 2e711436970f7593554fcf933177a3d155f4376f Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Wed, 2 Apr 2025 18:24:55 +0200 Subject: [PATCH 44/49] refactor: replace postNoBody with a more explicit postReturn --- src/connection/http.ts | 4 ---- src/users/index.ts | 6 ++++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/connection/http.ts b/src/connection/http.ts index 621a3f3b..1af076fd 100644 --- a/src/connection/http.ts +++ b/src/connection/http.ts @@ -120,10 +120,6 @@ export default class ConnectionREST { return this.http.post(path, payload, true, '') as Promise; }; - postNoBody = (path: string): Promise => { - return this.postReturn(path, null); - }; - postEmpty = (path: string, payload: B): Promise => { if (this.authEnabled) { return this.login().then((token) => this.http.post(path, payload, false, token)); diff --git a/src/users/index.ts b/src/users/index.ts index 29e0476c..0237ac96 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -119,14 +119,16 @@ const db = (connection: ConnectionREST): DBUsers => { ns.revokeRoles(roleNames, userId, { userType: 'db' }), create: (userId: string) => - connection.postNoBody(`/users/db/${userId}`).then((resp) => resp.apikey), + connection.postReturn(`/users/db/${userId}`, null).then((resp) => resp.apikey), delete: (userId: string) => connection .delete(`/users/db/${userId}`, null) .then(() => true) .catch(() => false), rotateKey: (userId: string) => - connection.postNoBody(`/users/db/${userId}/rotate-key`).then((resp) => resp.apikey), + connection + .postReturn(`/users/db/${userId}/rotate-key`, null) + .then((resp) => resp.apikey), activate: (userId: string) => connection .postEmpty(`/users/db/${userId}/activate`, null) From bf294e99d51b66a6aa3649b0874c4bf861a6f9a7 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Wed, 2 Apr 2025 18:57:46 +0200 Subject: [PATCH 45/49] chore: add documentation for dynamic user management --- src/roles/index.ts | 3 ++- src/users/index.ts | 57 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/roles/index.ts b/src/roles/index.ts index 139e9dee..6971cf4f 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -35,7 +35,8 @@ export interface Roles { */ byName: (roleName: string) => Promise; /** - * Retrieve the user IDs assigned to a role. + * Retrieve the user IDs assigned to a role. Each user has a qualifying user type, + * e.g. `'db_user' | 'db_env_user' | 'oidc'`. * * @param {string} roleName The name of the role to retrieve the assigned user IDs for. * @returns {Promise} The user IDs assigned to the role. diff --git a/src/users/index.ts b/src/users/index.ts index 0237ac96..037a3a34 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -55,26 +55,74 @@ export interface Users extends UsersBase { /** Operations supported for namespaced 'db' users.*/ export interface DBUsers extends UsersBase { /** - * Retrieve the roles assigned to a user. + * Retrieve the roles assigned to a 'db_user' user. * * @param {string} userId The ID of the user to retrieve the assigned roles for. * @returns {Promise>} A map of role names to their respective roles. */ getAssignedRoles: (userId: string, opts?: GetAssignedRolesOptions) => Promise>; + /** Create a new 'db_user' user. + * + * @param {string} userId The ID of the user to create. Must consist of valid URL characters only. + * @returns {Promise} API key for the newly created user. + */ create: (userId: string) => Promise; + + /** + * Delete a 'db_user' user. It is not possible to delete 'db_env_user' users programmatically. + * + * @param {string} userId The ID of the user to delete. + * @returns {Promise} `true` if the user has been successfully deleted. + */ delete: (userId: string) => Promise; + + /** + * Rotate the API key of a 'db_user' user. The old API key becomes invalid. + * API keys of 'db_env_user' users are defined in the server's environment + * and cannot be modified programmatically. + * + * @param {string} userId The ID of the user to create a new API key for. + * @returns {Promise} New API key for the user. + */ rotateKey: (userId: string) => Promise; + + /** + * Activate 'db_user' user. + * + * @param {string} userId The ID of the user to activate. + * @returns {Promise} `true` if the user has been successfully activated. + */ activate: (userId: string) => Promise; + + /** + * Deactivate 'db_user' user. + * + * @param {string} userId The ID of the user to deactivate. + * @returns {Promise} `true` if the user has been successfully deactivated. + */ deactivate: (userId: string) => Promise; + + /** + * Retrieve information about the 'db_user' / 'db_env_user' user. + * + * @param {string} userId The ID of the user to get. + * @returns {Promise} ID, status, and assigned roles of a 'db_*' user. + */ byName: (userId: string) => Promise; + + /** + * List all 'db_user' / 'db_env_user' users. + * + * @returns {Promise} ID, status, and assigned roles for each 'db_*' user. + */ listAll: () => Promise; } /** Operations supported for namespaced 'oidc' users.*/ export interface OIDCUsers extends UsersBase { /** - * Retrieve the roles assigned to a user. + * Retrieve the roles assigned to an 'oidc' user. * * @param {string} userId The ID of the user to retrieve the assigned roles for. * @returns {Promise>} A map of role names to their respective roles. @@ -167,6 +215,7 @@ interface NamespacedUsers { revokeRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => Promise; } +/** Implementation of the operations common to 'db', 'oidc', and legacy users. */ const baseUsers = (connection: ConnectionREST): UsersBase => { const ns = namespacedUsers(connection); return { @@ -175,13 +224,13 @@ const baseUsers = (connection: ConnectionREST): UsersBase => { }; }; +/** Implementation of the operations common to 'db' and 'oidc' users. */ const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { return { getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssignedRolesOptions) => connection .get( - `/authz/users/${userId}/roles/${userType}${ - opts?.includePermissions ? '?&includeFullRoles=true' : '' + `/authz/users/${userId}/roles/${userType}${opts?.includePermissions ? '?&includeFullRoles=true' : '' }` ) .then(Map.roles), From 67b675e935f72f851c1d030f8fd0475992d85a83 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Wed, 2 Apr 2025 19:20:26 +0200 Subject: [PATCH 46/49] chore: lint and format --- src/users/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/users/index.ts b/src/users/index.ts index 037a3a34..2fa51d51 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -230,7 +230,8 @@ const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssignedRolesOptions) => connection .get( - `/authz/users/${userId}/roles/${userType}${opts?.includePermissions ? '?&includeFullRoles=true' : '' + `/authz/users/${userId}/roles/${userType}${ + opts?.includePermissions ? '?&includeFullRoles=true' : '' }` ) .then(Map.roles), From aa04c920ef9840da0c62226a1fc2373030efe5e3 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 3 Apr 2025 15:04:16 +0200 Subject: [PATCH 47/49] test: use different port binding for backup/unit.test.ts Seems like Node 18 does not automatically 'resolve' port collisions, so 2 test running Express servers on the same ports will interfere with one another. --- src/collections/backup/unit.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/collections/backup/unit.test.ts b/src/collections/backup/unit.test.ts index 8cf971a6..f5131d03 100644 --- a/src/collections/backup/unit.test.ts +++ b/src/collections/backup/unit.test.ts @@ -109,8 +109,8 @@ describe('Mock testing of backup cancellation', () => { let mock: CancelMock; beforeAll(async () => { - mock = await CancelMock.use('1.27.0', 8958, 8959); - client = await weaviate.connectToLocal({ port: 8958, grpcPort: 8959 }); + mock = await CancelMock.use('1.27.0', 8912, 8913); + client = await weaviate.connectToLocal({ port: 8912, grpcPort: 8913 }); }); it('should throw while waiting for creation if backup is cancelled in the meantime', async () => { @@ -133,7 +133,7 @@ describe('Mock testing of backup cancellation', () => { }); it('should return false if creation backup does not exist', async () => { - const success = await client.backup.cancel({ backupId: `${BACKUP_ID}4`, backend: BACKEND }); + const success = await client.backup.cancel({ backupId: `${BACKUP_ID}-unknown`, backend: BACKEND }); expect(success).toBe(false); }); From a140c534dbda0f8b5b43109bcf12513b46cef5bb Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 3 Apr 2025 15:17:57 +0200 Subject: [PATCH 48/49] feat: add revokeKey option to /deactivate --- src/users/index.ts | 8 ++++---- src/users/integration.test.ts | 2 +- src/users/types.ts | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index 2fa51d51..7b2c608a 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -8,7 +8,7 @@ import { } from '../openapi/types.js'; import { Role } from '../roles/types.js'; import { Map } from '../roles/util.js'; -import { AssignRevokeOptions, GetAssignedRolesOptions, User, UserDB } from './types.js'; +import { AssignRevokeOptions, DeactivateOptions, GetAssignedRolesOptions, User, UserDB } from './types.js'; /** * Operations supported for 'db', 'oidc', and legacy (non-namespaced) users. @@ -101,7 +101,7 @@ export interface DBUsers extends UsersBase { * @param {string} userId The ID of the user to deactivate. * @returns {Promise} `true` if the user has been successfully deactivated. */ - deactivate: (userId: string) => Promise; + deactivate: (userId: string, opts?: DeactivateOptions) => Promise; /** * Retrieve information about the 'db_user' / 'db_env_user' user. @@ -182,9 +182,9 @@ const db = (connection: ConnectionREST): DBUsers => { .postEmpty(`/users/db/${userId}/activate`, null) .then(() => true) .catch(expectCode(409)), - deactivate: (userId: string) => + deactivate: (userId: string, opts?: DeactivateOptions) => connection - .postEmpty(`/users/db/${userId}/deactivate`, null) + .postEmpty(`/users/db/${userId}/deactivate`, opts || null) .then(() => true) .catch(expectCode(409)), byName: (userId: string) => connection.get(`/users/db/${userId}`, true).then(Map.dbUser), diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 91f7b453..0c442bfe 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -85,7 +85,7 @@ requireAtLeast( await expectDave().toHaveProperty('active', false); // Second deactivation is a no-op - await expect(client.users.db.deactivate('dynamic-dave')).resolves.toEqual(true); + await expect(client.users.db.deactivate('dynamic-dave', { revokeKey: true })).resolves.toEqual(true); await client.users.db.delete('dynamic-dave'); await expectDave(false).toHaveProperty('code', 404); diff --git a/src/users/types.ts b/src/users/types.ts index 6dee0ad7..b4a9d59d 100644 --- a/src/users/types.ts +++ b/src/users/types.ts @@ -20,3 +20,6 @@ export type GetAssignedRolesOptions = { /** Optional arguments to /assign and /revoke endpoints. */ export type AssignRevokeOptions = { userType?: WeaviateUserTypeInternal }; + +/** Optional arguments to /deactivate endpoint. */ +export type DeactivateOptions = { revokeKey?: boolean }; From 3af20832c2afadff64720ed46b2e4d2d3297aa13 Mon Sep 17 00:00:00 2001 From: Tommy Smith Date: Thu, 3 Apr 2025 15:07:09 +0100 Subject: [PATCH 49/49] Remove `--tag next` from CI publish in preparation for merging --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index c65e7f40..4db465ef 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -125,7 +125,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build - - run: npm publish --tag next + - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} - run: npm run docs