diff --git a/CHANGELOG.md b/CHANGELOG.md index 484f2996..a3495756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`generative-deepseek`** — New `GenerativeConfigFactory.Deepseek(...)` collection config and `GenerativeProviderFactory.Deepseek(...)` runtime provider for the `generative-deepseek` module. Requires Weaviate ≥ 1.36.19. +- **Incremental Backups** — `BackupCreateRequest.IncrementalBaseBackupId` names an existing backup to build on, so files unchanged since that backup are not copied again; `Backup.IncrementalBaseBackupId` surfaces it on `List()` and `GetStatus()`. `Create` throws `WeaviateVersionMismatchException` below 1.37.0, the documented feature floor. The server has accepted the field on create since 1.34.18, but only returns it on read from 1.37.6. +- **`apiEndpoint` on `Multi2VecGoogle`** — Both `VectorizerFactory.Multi2VecGoogle(...)` overloads take an optional `apiEndpoint`, which selects the Gemini API (`generativelanguage.googleapis.com`) instead of Vertex AI. Both `Multi2VecGoogleGemini(...)` overloads also gained an optional `dimensions`. + +### Fixed + +- **Backup `Size` Dropped on List** — `BackupClient.List()` discarded the size and incremental base id it had already parsed from the response, so `Backup.Size` was null for every listed backup. Both fields are now mapped. +- **Aggregate Zeros for Absent Values** — Int, Number, Boolean, Text and Date results, `Count` included, reported `0`, `0.0` or `false` where the server had sent no value at all. Every scalar is now presence-checked and left null when unset, so an empty aggregation is distinguishable from a genuine zero. +- **`Multi2VecGoogleGemini` Emitted a Non-Existent Module** — The vectorizer declared the module name `multi2vec-google-gemini`, which no Weaviate server provides, so a collection configured with it could never be created. `VectorizerFactory.Multi2VecGoogleGemini(...)` now emits `multi2vec-google` with the Gemini API endpoint. + +### Changed + +- **`Multi2VecGoogleGemini` is now an `[Obsolete]` shim over `Multi2VecGoogle`** — This breaks at runtime, not only at compile time: the factory now returns a `Multi2VecGoogle`, so `is`/`as`/pattern matches on `Multi2VecGoogleGemini` silently stop matching, and a `switch` with arms for both types no longer compiles (`CS8120: The switch case is unreachable`). Bind the result as `Multi2VecGoogle` or `VectorizerConfig`. +- **`Multi2VecGoogle.ProjectId` and `.Location` are no longer `required`** — Both getters changed from `string!` to `string?` so the Gemini endpoint, which is scoped to neither a project nor a region, can omit them. Callers with nullable reference types enabled may see CS8600/CS8601. +- **`Aggregate.Boolean` counts are nullable** — `PercentageTrue`, `PercentageFalse`, `TotalTrue` and `TotalFalse` changed from `double`/`long` to `double?`/`long?`. +- **`BackupCreateRequest` gained a trailing optional parameter** — Its primary constructor and `Deconstruct` went from six parameters to seven, so six-element positional deconstruction (`var (id, backend, inc, exc, cpu, comp) = req;`) no longer compiles; deconstruct seven elements instead. +- **Binary compatibility** — The signature changes above are source-compatible for callers using named arguments, but not binary-compatible: assemblies compiled against 1.1.0 throw `MissingMethodException` until recompiled. + ### Removed - **`ReplicationAsyncConfig.MaxWorkers` and `ReplicationAsyncConfig.AliveNodesCheckingFrequency`** — Both fields have been no-ops on the server since Weaviate 1.37.3 and are now removed from the user-facing model, the OpenAPI spec, and the generated DTO. Existing code that sets these properties will not compile after upgrading; no behavioral change results from the removal. +- **`Multi2VecGoogleGemini.Model`** — Removed along with the rest of the type's own members; the base `Multi2VecGoogle` declares the same setting as `ModelId`. Migrate `.Model` → `.ModelId`. --- @@ -67,7 +88,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Vectorizers -- **Audio Field Support** ([#302](https://github.com/weaviate/weaviate-csharp-client/pull/302)): `Multi2VecGoogle` and `Multi2VecGoogleGemini` vectorizers now support audio field configurations with configurable per-field weights. +- **Audio Field Support** ([#302](https://github.com/weaviate/weaviate-csharp-client/pull/302)): `Multi2VecGoogle` and `Multi2VecGoogleGemini` vectorizers now support audio field configurations with configurable per-field weights. (`Multi2VecGoogleGemini` is deprecated in favour of `Multi2VecGoogle` — see Unreleased.) #### API Ergonomics @@ -105,7 +126,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Weaviate 1.36 support: HFresh vector index, async replication config, property index deletion - Critical fix: gRPC vector serialization no longer doubles dimensions for non-`float[]` vectors - Opt-in structured logging via `ILoggerFactory` -- New vectorizers: `Multi2VecGoogleGemini` and `Multi2MultivecWeaviate` +- New vectorizers: `Multi2VecGoogleGemini` (never functional — see Unreleased) and `Multi2MultivecWeaviate` ### Added @@ -119,7 +140,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Vectorizers -- **Multi2VecGoogleGemini** ([#297](https://github.com/weaviate/weaviate-csharp-client/pull/297)): New vectorizer calling the Google Gemini API directly. Supports image, text, and video field weighting. No project ID or location required (unlike the Vertex AI variant). Defaults to `generativelanguage.googleapis.com`. +- **Multi2VecGoogleGemini** ([#297](https://github.com/weaviate/weaviate-csharp-client/pull/297)): New vectorizer calling the Google Gemini API directly. Supports image, text, and video field weighting. No project ID or location required (unlike the Vertex AI variant). Defaults to `generativelanguage.googleapis.com`. **Correction:** this vectorizer declared the module name `multi2vec-google-gemini`, which no Weaviate server provides, so it could never create a collection; fixed in Unreleased. - **Multi2MultivecWeaviate** ([#291](https://github.com/weaviate/weaviate-csharp-client/pull/291)): Support for the `multi2multivec-weaviate` vectorizer, which produces multi-vector embeddings using Weaviate's built-in model. - **Cohere Reranker `BaseURL`** ([#287](https://github.com/weaviate/weaviate-csharp-client/pull/287)): Added `BaseURL` property to `RerankerCohereConfig` and a corresponding parameter to `RerankerConfigFactory.Cohere()`, enabling self-hosted or regional Cohere endpoints. diff --git a/ci/docker-compose.yml b/ci/docker-compose.yml index b197578c..72d71416 100644 --- a/ci/docker-compose.yml +++ b/ci/docker-compose.yml @@ -22,7 +22,7 @@ services: QUERY_DEFAULTS_LIMIT: 25 AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' PERSISTENCE_DATA_PATH: '/var/lib/weaviate' - ENABLE_MODULES: text2vec-transformers,text2vec-cohere,backup-filesystem,generative-dummy,generative-anyscale,reranker-dummy,reranker-cohere,text2vec-ollama,generative-ollama + ENABLE_MODULES: text2vec-transformers,text2vec-cohere,backup-filesystem,generative-dummy,generative-anyscale,reranker-dummy,reranker-cohere,text2vec-ollama,generative-ollama,multi2vec-google,generative-deepseek BACKUP_FILESYSTEM_PATH: "/tmp/backups" EXPORT_ENABLED: 'true' EXPORT_DEFAULT_PATH: "/tmp/exports" diff --git a/docs/AGGREGATE_RESULT_ACCESSORS.md b/docs/AGGREGATE_RESULT_ACCESSORS.md index 9fec2b24..cfe55d8a 100644 --- a/docs/AGGREGATE_RESULT_ACCESSORS.md +++ b/docs/AGGREGATE_RESULT_ACCESSORS.md @@ -107,11 +107,11 @@ if (quantityAgg != null) Console.WriteLine($"Total quantity: {quantityAgg.Sum}"); } -// Access boolean aggregation +// Access boolean aggregation (its members are null when the server did not send them) var inStockAgg = result.Boolean("inStock"); -if (inStockAgg != null) +if (inStockAgg?.PercentageTrue is { } inStockPercentage) { - Console.WriteLine($"In stock: {inStockAgg.PercentageTrue:P0}"); + Console.WriteLine($"In stock: {inStockPercentage:P0}"); } // Access date aggregation @@ -157,9 +157,13 @@ if (result.TryGetText("category", out var category)) } // Generic TryGet -if (result.TryGet("inStock", out var stockAgg)) +if ( + result.TryGet("inStock", out var stockAgg) + && stockAgg.TotalTrue is { } inStock + && stockAgg.TotalFalse is { } outOfStock +) { - Console.WriteLine($"In stock: {stockAgg.TotalTrue}, Out of stock: {stockAgg.TotalFalse}"); + Console.WriteLine($"In stock: {inStock}, Out of stock: {outOfStock}"); } ``` @@ -196,7 +200,11 @@ result.Match("field", text: t => Console.WriteLine($"Text: {t.Count} occurrences"), integer: i => Console.WriteLine($"Integer: sum={i.Sum}"), number: n => Console.WriteLine($"Number: mean={n.Mean}"), - boolean: b => Console.WriteLine($"Boolean: {b.PercentageTrue:P0} true"), + boolean: b => + { + if (b.PercentageTrue is { } percentage) + Console.WriteLine($"Boolean: {percentage:P0} true"); + }, date: d => Console.WriteLine($"Date range: {d.Minimum} to {d.Maximum}") ); @@ -205,7 +213,10 @@ var description = result.Match("field", text: t => $"Text with {t.TopOccurrences.Count} unique values", integer: i => $"Integer ranging from {i.Minimum} to {i.Maximum}", number: n => $"Number with mean {n.Mean:F2}", - boolean: b => $"Boolean: {b.PercentageTrue:P0} true", + boolean: b => + b.PercentageTrue is { } percentage + ? $"Boolean: {percentage:P0} true" + : "Boolean: percentage not returned", date: d => $"Dates from {d.Minimum:d} to {d.Maximum:d}" ); @@ -223,7 +234,10 @@ foreach (var (name, _) in result.Properties) text: t => $"{name}: {t.Count} items, top: {t.TopOccurrences.FirstOrDefault()?.Value}", integer: i => $"{name}: range [{i.Minimum}, {i.Maximum}], sum: {i.Sum}", number: n => $"{name}: range [{n.Minimum:F2}, {n.Maximum:F2}], mean: {n.Mean:F2}", - boolean: b => $"{name}: {b.TotalTrue} true, {b.TotalFalse} false", + boolean: b => + b.TotalTrue is { } trueCount && b.TotalFalse is { } falseCount + ? $"{name}: {trueCount} true, {falseCount} false" + : $"{name}: boolean counts not returned", date: d => $"{name}: {d.Minimum:d} to {d.Maximum:d}" ); @@ -312,10 +326,13 @@ For boolean properties: | Property | Type | Description | |----------|------|-------------| | `Count` | `long?` | Number of values | -| `TotalTrue` | `long` | Count of true values | -| `TotalFalse` | `long` | Count of false values | -| `PercentageTrue` | `double` | Percentage of true values (0-1) | -| `PercentageFalse` | `double` | Percentage of false values (0-1) | +| `TotalTrue` | `long?` | Count of true values | +| `TotalFalse` | `long?` | Count of false values | +| `PercentageTrue` | `double?` | Percentage of true values (0-1) | +| `PercentageFalse` | `double?` | Percentage of false values (0-1) | + +Every member is nullable: null means the server did not send that value, which is not the same as +zero. Check before formatting — interpolating a null renders as an empty string. ### Aggregate.Date diff --git a/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs b/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs index d7c7c170..cce1cc3e 100644 --- a/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs +++ b/src/Weaviate.Client.Tests/Integration/TestCollectionAggregate.cs @@ -903,4 +903,82 @@ await collectionClient.Data.Insert( break; } } + + /// + /// Two ways a scalar goes missing, on one collection: a filter that matches nothing, and a + /// sub-metric the caller never asked for. Both leave the optional proto field unset, and the + /// client must report null rather than the zero of the field's type — a caller cannot tell a + /// returned 0 apart from a genuine aggregate of zeros. + /// + [Fact] + public async Task Test_OverAll_Absent_Metrics_Are_Null_Not_Zero() + { + var collectionClient = await CollectionFactory( + properties: new[] + { + Property.Text("text"), + Property.Int("int"), + Property.Number("float"), + } + ); + + await collectionClient.Data.Insert( + new + { + text = "one", + @int = 7, + @float = 7.5, + }, + cancellationToken: TestContext.Current.CancellationToken + ); + + // Nothing matches: the metrics were requested, but there is nothing to compute them over. + var noMatches = await collectionClient.Aggregate.OverAll( + filters: Filter.Property("text").IsEqual("no-such-value"), + returnMetrics: + [ + Metrics.ForProperty("int").Integer(maximum: true, mean: true, sum: true), + Metrics.ForProperty("float").Number(maximum: true, mean: true, sum: true), + ], + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.Equal(0, noMatches.TotalCount); + + var emptyInteger = Assert.IsType(noMatches.Properties["int"]); + Assert.Null(emptyInteger.Maximum); + Assert.Null(emptyInteger.Mean); + Assert.Null(emptyInteger.Sum); + + var emptyNumber = Assert.IsType(noMatches.Properties["float"]); + Assert.Null(emptyNumber.Maximum); + Assert.Null(emptyNumber.Mean); + Assert.Null(emptyNumber.Sum); + + // Objects do match here, so the nulls come from the metric selection alone. + var unrequested = await collectionClient.Aggregate.OverAll( + returnMetrics: + [ + Metrics.ForProperty("int").Integer(maximum: true), + Metrics.ForProperty("float").Number(mean: true), + ], + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.Equal(1, unrequested.TotalCount); + + var integer = Assert.IsType(unrequested.Properties["int"]); + Assert.Equal(7, integer.Maximum); + Assert.Null(integer.Mean); + Assert.Null(integer.Median); + Assert.Null(integer.Minimum); + Assert.Null(integer.Mode); + Assert.Null(integer.Sum); + + var number = Assert.IsType(unrequested.Properties["float"]); + Assert.Equal(7.5, number.Mean); + Assert.Null(number.Maximum); + Assert.Null(number.Minimum); + Assert.Null(number.Sum); + } } diff --git a/src/Weaviate.Client.Tests/Integration/TestCollections.cs b/src/Weaviate.Client.Tests/Integration/TestCollections.cs index b81b6953..73eeffea 100644 --- a/src/Weaviate.Client.Tests/Integration/TestCollections.cs +++ b/src/Weaviate.Client.Tests/Integration/TestCollections.cs @@ -193,6 +193,55 @@ public async Task Collection_Creates_And_Retrieves_Generative_Config() Assert.IsType(collection.GenerativeConfig); } + /// + /// Tests that a generative-deepseek collection round-trips through a real server. The module + /// validates its own settings on create (temperature, both penalties and topP all have + /// ranges, and baseURL is parsed), so a misspelt key or a wrong type shows up here rather + /// than silently falling back to a default. + /// + /// + /// Integral floats only; the server drops fractional floats for this module on named-vector + /// classes. See PR #368. Fractional values are covered by the unit test instead. + /// + [Fact] + public async Task Collection_Creates_And_Retrieves_GenerativeDeepseek_Config() + { + RequireModule("generative-deepseek"); + + // Arrange + var collectionClient = await CollectionFactory( + properties: [Property.Text("Name")], + generativeConfig: Configure.Generative.Deepseek( + model: "deepseek-chat", + temperature: 1, + maxTokens: 2048, + frequencyPenalty: 0, + presencePenalty: 0, + topP: 1, + baseURL: "https://api.deepseek.com", + stop: ["\n\n"] + ) + ); + + // Act + var collection = await _weaviate + .Collections.Use(collectionClient.Name) + .Config.Get(TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(collection); + var deepseek = Assert.IsType(collection.GenerativeConfig); + Assert.Equal("deepseek-chat", deepseek.Model); + Assert.Equal(1, deepseek.Temperature); + Assert.Equal(2048, deepseek.MaxTokens); + Assert.Equal(0, deepseek.FrequencyPenalty); + Assert.Equal(0, deepseek.PresencePenalty); + Assert.Equal(1, deepseek.TopP); + Assert.Equal("https://api.deepseek.com", deepseek.BaseURL); + Assert.NotNull(deepseek.Stop); + Assert.Equal(["\n\n"], deepseek.Stop); + } + /// /// Tests that test collections export /// diff --git a/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs new file mode 100644 index 00000000..b25d4e20 --- /dev/null +++ b/src/Weaviate.Client.Tests/Integration/TestVectorizers.cs @@ -0,0 +1,95 @@ +namespace Weaviate.Client.Tests.Integration; + +using Weaviate.Client.Models; + +/// +/// The vectorizer module tests class. Covers vectorizer configurations end to end, i.e. that the +/// module name and settings the client emits are ones a real server accepts. +/// +/// +public class TestVectorizers : IntegrationTests +{ + /// + /// Tests that the Google Gemini multimodal factory produces a collection the server accepts. + /// + [Fact] + public async Task Test_Multi2VecGoogleGemini_Creates_Collection() + { + RequireModule("multi2vec-google"); + // The module is on every lane, but its Gemini path (apiEndpoint) only lands in 1.34.20; + // older builds still demand the Vertex projectId/location this factory omits. + RequireVersion("1.34.20"); + + var collection = await CollectionFactory( + name: "TestMulti2VecGoogleGemini", + properties: [Property.Text("text"), Property.Blob("image")], + vectorConfig: Configure.Vector( + "default", + v => + v.Multi2VecGoogleGemini( + imageFields: ["image"], + textFields: ["text"], + dimensions: 512 + ) + ) + ); + + var config = await collection.Config.Get( + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.NotNull(config); + var vectorizer = config.VectorConfig["default"].Vectorizer; + var google = Assert.IsType(vectorizer); + + Assert.Equal("multi2vec-palm", google.Identifier); + Assert.Equal("generativelanguage.googleapis.com", google.ApiEndpoint); + Assert.Equal(512, google.Dimensions); + // The factory has no vectorizeClassName: nothing is sent, nothing stored or defaulted back. + Assert.Null(google.VectorizeCollectionName); + Assert.NotNull(google.ImageFields); + Assert.Equal(["image"], google.ImageFields); + Assert.NotNull(google.TextFields); + Assert.Equal(["text"], google.TextFields); + // Vertex-only settings: the Gemini API has neither, and the server must not echo them. + Assert.Null(google.ProjectId); + Assert.Null(google.Location); + } + + /// + /// Tests that the Vertex AI factory creates a collection with project id and location intact. + /// + [Fact] + public async Task Test_Multi2VecGoogle_Vertex_Creates_Collection() + { + RequireModule("multi2vec-google"); + + var collection = await CollectionFactory( + name: "TestMulti2VecGoogleVertex", + properties: [Property.Text("text"), Property.Blob("image")], + vectorConfig: Configure.Vector( + "default", + v => + v.Multi2VecGoogle( + projectId: "my-project", + location: "us-central1", + imageFields: ["image"], + textFields: ["text"] + ) + ) + ); + + var config = await collection.Config.Get( + cancellationToken: TestContext.Current.CancellationToken + ); + + Assert.NotNull(config); + var vectorizer = config.VectorConfig["default"].Vectorizer; + var google = Assert.IsType(vectorizer); + + Assert.Equal("multi2vec-palm", google.Identifier); + Assert.Equal("my-project", google.ProjectId); + Assert.Equal("us-central1", google.Location); + Assert.Null(google.ApiEndpoint); + } +} diff --git a/src/Weaviate.Client.Tests/Integration/_Integration.cs b/src/Weaviate.Client.Tests/Integration/_Integration.cs index 600927fc..2753b840 100644 --- a/src/Weaviate.Client.Tests/Integration/_Integration.cs +++ b/src/Weaviate.Client.Tests/Integration/_Integration.cs @@ -418,6 +418,20 @@ protected bool ServerVersionIsInRange(string minimumVersion, string? maximumVers return VersionIsInRange(_weaviate.WeaviateVersion, minimumVersion, maximumVersion); } + /// + /// Skips the test unless the connected server has the named module enabled. + /// A module the server does not load cannot be named in a collection config at all, so + /// tests that create such a collection are meaningless — not failing — without it. + /// + /// The module name, e.g. multi2vec-google. + protected void RequireModule(string moduleName) + { + if (_weaviate.Meta?.Modules.ContainsKey(moduleName) != true) + { + Assert.Skip($"Weaviate module '{moduleName}' is not enabled on the test server."); + } + } + /// /// Requires the version using the specified minimum version /// diff --git a/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs b/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs index 844fe272..37277b82 100644 --- a/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs +++ b/src/Weaviate.Client.Tests/Unit/TestAggregateResultAccessors.cs @@ -1,4 +1,5 @@ using Weaviate.Client.Models; +using Agg = Weaviate.Client.Grpc.Protobuf.V1.AggregateReply.Types.Aggregations.Types.Aggregation; namespace Weaviate.Client.Tests.Unit; @@ -779,4 +780,142 @@ public void Group_Match_Func_ReturnsCorrectValue() } #endregion + + #region Grpc Presence Mapping + + /// + /// Every scalar in the aggregate reply is optional in the proto. When the server + /// leaves one unset the client must report null, not the field type's zero — a returned 0, + /// 0.0 or false is indistinguishable from a real aggregate. Driven straight off the proto + /// message so an unset field is guaranteed, which a live server will not always produce: + /// it always fills the four boolean members in, so only this test pins them. + /// + [Fact] + public void FromGrpcProperty_UnsetScalars_MapToNull() + { + var integer = AggregateResult.FromGrpcProperty( + new Agg { Property = "intField", Int = new Agg.Types.Integer() } + ); + var typedInteger = Assert.IsType(integer); + Assert.Null(typedInteger.Count); + Assert.Null(typedInteger.Maximum); + Assert.Null(typedInteger.Mean); + Assert.Null(typedInteger.Median); + Assert.Null(typedInteger.Minimum); + Assert.Null(typedInteger.Mode); + Assert.Null(typedInteger.Sum); + + var number = AggregateResult.FromGrpcProperty( + new Agg { Property = "floatField", Number = new Agg.Types.Number() } + ); + var typedNumber = Assert.IsType(number); + Assert.Null(typedNumber.Count); + Assert.Null(typedNumber.Maximum); + Assert.Null(typedNumber.Mean); + Assert.Null(typedNumber.Median); + Assert.Null(typedNumber.Minimum); + Assert.Null(typedNumber.Mode); + Assert.Null(typedNumber.Sum); + + var boolean = AggregateResult.FromGrpcProperty( + new Agg { Property = "boolField", Boolean = new Agg.Types.Boolean() } + ); + var typedBoolean = Assert.IsType(boolean); + Assert.Null(typedBoolean.Count); + Assert.Null(typedBoolean.PercentageFalse); + Assert.Null(typedBoolean.PercentageTrue); + Assert.Null(typedBoolean.TotalFalse); + Assert.Null(typedBoolean.TotalTrue); + + var text = AggregateResult.FromGrpcProperty( + new Agg { Property = "textField", Text = new Agg.Types.Text() } + ); + var typedText = Assert.IsType(text); + Assert.Null(typedText.Count); + + var date = AggregateResult.FromGrpcProperty( + new Agg { Property = "dateField", Date = new Agg.Types.Date() } + ); + var typedDate = Assert.IsType(date); + Assert.Null(typedDate.Count); + } + + /// + /// The complement of the case above: a scalar the server did set is carried through, so the + /// presence checks do not swallow real values. A deliberate zero must survive as zero. + /// + [Fact] + public void FromGrpcProperty_SetScalars_MapToValues() + { + var integer = AggregateResult.FromGrpcProperty( + new Agg + { + Property = "intField", + Int = new Agg.Types.Integer + { + Count = 3, + Maximum = 0, + Mean = 0, + Median = 2, + Minimum = -5, + Mode = 1, + Sum = 0, + }, + } + ); + var typedInteger = Assert.IsType(integer); + Assert.Equal(3, typedInteger.Count); + Assert.Equal(0, typedInteger.Maximum); + Assert.Equal(0, typedInteger.Mean); + Assert.Equal(2, typedInteger.Median); + Assert.Equal(-5, typedInteger.Minimum); + Assert.Equal(1, typedInteger.Mode); + Assert.Equal(0, typedInteger.Sum); + + var number = AggregateResult.FromGrpcProperty( + new Agg + { + Property = "floatField", + Number = new Agg.Types.Number + { + Count = 2, + Maximum = 0, + Mean = 0, + Median = 1.5, + Minimum = -1.5, + Mode = 0, + Sum = 0, + }, + } + ); + var typedNumber = Assert.IsType(number); + Assert.Equal(0, typedNumber.Maximum); + Assert.Equal(0, typedNumber.Mean); + Assert.Equal(1.5, typedNumber.Median); + Assert.Equal(-1.5, typedNumber.Minimum); + Assert.Equal(0, typedNumber.Mode); + Assert.Equal(0, typedNumber.Sum); + + var boolean = AggregateResult.FromGrpcProperty( + new Agg + { + Property = "boolField", + Boolean = new Agg.Types.Boolean + { + Count = 4, + PercentageFalse = 0, + PercentageTrue = 1, + TotalFalse = 0, + TotalTrue = 4, + }, + } + ); + var typedBoolean = Assert.IsType(boolean); + Assert.Equal(0, typedBoolean.PercentageFalse); + Assert.Equal(1, typedBoolean.PercentageTrue); + Assert.Equal(0, typedBoolean.TotalFalse); + Assert.Equal(4, typedBoolean.TotalTrue); + } + + #endregion } diff --git a/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs b/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs index f7daeb46..50c33901 100644 --- a/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs +++ b/src/Weaviate.Client.Tests/Unit/TestBackupClient.cs @@ -9,6 +9,40 @@ namespace Weaviate.Client.Tests.Unit; /// public class TestBackupClient { + /// + /// A create response body, reused by the incremental-backup cases below. + /// + private const string CreateResponseJson = """ + { + "id": "my-backup", + "backend": "filesystem", + "status": "STARTED", + "path": "/backups" + } + """; + + /// + /// A client that answers POST /v1/backups with , pinned to a + /// server version so the incremental-backup version gate can be exercised. + /// + private static (WeaviateClient Client, MockHttpMessageHandler Handler) CreateBackupClient( + string serverVersion + ) => + MockWeaviateClient.CreateWithMockHandler( + syncHandler: req => + req.Method == HttpMethod.Post + ? new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + CreateResponseJson, + System.Text.Encoding.UTF8, + "application/json" + ), + } + : null!, + serverVersion: serverVersion + ); + /// /// CancelRestore() must issue a DELETE to /v1/backups/{backend}/{id}/restore. /// @@ -58,10 +92,10 @@ await client.Backup.CancelRestore( } /// - /// GetStatus should populate Size from the response. + /// GetStatus should populate Size and the incremental base id from the response. /// [Fact] - public async Task GetStatus_SizeIsPopulatedFromResponse() + public async Task GetStatus_PopulatesSizeAndIncrementalBaseBackupId() { var json = """ { @@ -69,7 +103,8 @@ public async Task GetStatus_SizeIsPopulatedFromResponse() "status": "SUCCESS", "path": "/backups", "backend": "filesystem", - "size": 1.5 + "size": 1.5, + "incremental_base_backup_id": "base-backup" } """; @@ -87,6 +122,7 @@ public async Task GetStatus_SizeIsPopulatedFromResponse() ); Assert.Equal(1.5, backup.Size); + Assert.Equal("base-backup", backup.IncrementalBaseBackupId); } /// @@ -216,4 +252,100 @@ BackupStatus expected Assert.Equal(expected, backup.Status); } + + /// + /// Create() must put IncrementalBaseBackupId on the wire under the spec's + /// incremental_base_backup_id key, or the client silently takes a full backup instead; + /// and omit the key when no base is asked for, which is why a plain backup still works on a + /// server below 1.37.0 — the gate is on the field, not the operation. + /// + [Theory] + [InlineData("1.37.0", "base-backup")] + [InlineData("1.36.0", null)] + public async Task Create_SendsIncrementalBaseBackupId_OnlyWhenRequested( + string serverVersion, + string? incrementalBaseBackupId + ) + { + var (client, handler) = CreateBackupClient(serverVersion); + + await client.Backup.Create( + new BackupCreateRequest( + "my-backup", + new FilesystemBackend("/backups"), + IncrementalBaseBackupId: incrementalBaseBackupId + ), + TestContext.Current.CancellationToken + ); + + Assert.NotNull(handler.LastRequest); + var body = await handler.LastRequest!.Content!.ReadAsStringAsync( + TestContext.Current.CancellationToken + ); + + if (incrementalBaseBackupId is null) + Assert.DoesNotContain("incremental_base_backup_id", body); + else + Assert.Contains($"\"incremental_base_backup_id\":\"{incrementalBaseBackupId}\"", body); + } + + /// + /// The 1.37.0 gate is the documented feature floor, not the wire field's age: asking for an + /// incremental backup against an older server must fail rather than silently taking a full one. + /// + [Fact] + public async Task Create_Throws_WhenIncrementalRequestedOnOlderServer() + { + var (client, _) = CreateBackupClient("1.36.0"); + + var exception = await Assert.ThrowsAsync(async () => + await client.Backup.Create( + new BackupCreateRequest( + "my-backup", + new FilesystemBackend("/backups"), + IncrementalBaseBackupId: "base-backup" + ), + TestContext.Current.CancellationToken + ) + ); + + Assert.Equal(new Version(1, 37, 0), exception.RequiredVersion); + Assert.Equal(new Version(1, 36, 0), exception.ActualVersion); + } + + /// + /// List() parsed Size and the incremental base id and then dropped both; they must survive + /// onto the model, and be null rather than empty when the server omits them. + /// + [Theory] + [InlineData( + """[{"id":"my-backup","classes":["Article"],"status":"SUCCESS","startedAt":"2026-08-14T10:00:00Z","completedAt":"2026-08-14T10:05:00Z","size":2.5,"incremental_base_backup_id":"base-backup"}]""", + "base-backup" + )] + [InlineData( + """[{"id":"my-backup","classes":["Article"],"status":"SUCCESS","size":2.5}]""", + null + )] + public async Task List_PopulatesSizeAndIncrementalBaseBackupId( + string json, + string? expectedIncrementalBaseBackupId + ) + { + var (client, _) = MockWeaviateClient.CreateWithMockHandler( + syncHandler: _ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"), + } + ); + + var backups = await client.Backup.List( + BackupStorageProvider.Filesystem, + TestContext.Current.CancellationToken + ); + + var backup = Assert.Single(backups); + Assert.Equal("my-backup", backup.Id); + Assert.Equal(2.5, backup.Size); + Assert.Equal(expectedIncrementalBaseBackupId, backup.IncrementalBaseBackupId); + } } diff --git a/src/Weaviate.Client.Tests/Unit/TestCollection.cs b/src/Weaviate.Client.Tests/Unit/TestCollection.cs index 7f5ca918..9cc8290b 100644 --- a/src/Weaviate.Client.Tests/Unit/TestCollection.cs +++ b/src/Weaviate.Client.Tests/Unit/TestCollection.cs @@ -388,6 +388,80 @@ public void Collection_GenerativeGoogleVertex_Serializes_And_RoundTrips_Location Assert.Equal("us-east4", typed.Location); } + /// + /// Tests that the DeepSeek generative config serializes every setting under the exact key + /// the module reads, omits what is unset, and round-trips. The keys are pinned literally + /// against modules/generative-deepseek/config/class_settings.go, in particular + /// baseURL: the camelCase policy happens to produce the right casing here, but a + /// baseUrl/baseURL slip is silent — the server just ignores the unknown key + /// and falls back to its default endpoint. + /// + [Fact] + public void Collection_GenerativeDeepseek_Serializes_And_RoundTrips() + { + // Arrange + var full = Assert.IsType( + Configure.Generative.Deepseek( + model: "deepseek-chat", + temperature: 0.7, + maxTokens: 2048, + frequencyPenalty: 0.5, + presencePenalty: 0.25, + topP: 0.9, + baseURL: "https://api.deepseek.com", + stop: ["\n\n"] + ) + ); + var empty = Assert.IsType(Configure.Generative.Deepseek()); + + // Act + var jsonFull = JsonSerializer.Serialize( + full, + Rest.WeaviateRestClient.RestJsonSerializerOptions + ); + var jsonEmpty = JsonSerializer.Serialize( + empty, + Rest.WeaviateRestClient.RestJsonSerializerOptions + ); + + // Assert + Assert.Equal("generative-deepseek", full.Type); + Assert.Contains("\"model\":\"deepseek-chat\"", jsonFull); + Assert.Contains("\"temperature\":0.7", jsonFull); + Assert.Contains("\"maxTokens\":2048", jsonFull); + Assert.Contains("\"frequencyPenalty\":0.5", jsonFull); + Assert.Contains("\"presencePenalty\":0.25", jsonFull); + Assert.Contains("\"topP\":0.9", jsonFull); + Assert.Contains("\"baseURL\":\"https://api.deepseek.com\"", jsonFull); + Assert.Contains("\"stop\":[\"\\n\\n\"]", jsonFull); + // Every generative config also writes its own "type": IGenerativeConfig.Type carries + // [JsonIgnore] but attributes on an interface member do not apply to the implementing + // property. Pre-existing for all providers and accepted by the server; asserted here so + // DeepSeek is shown to behave the same rather than differently. + Assert.Contains("\"type\":\"generative-deepseek\"", jsonFull); + + // Nothing set means nothing else sent, so the server keeps its own defaults. + Assert.Equal("{\"type\":\"generative-deepseek\"}", jsonEmpty); + + var roundTripped = GenerativeConfigSerialization.Factory( + GenerativeConfig.Deepseek.TypeValue, + JsonSerializer.Deserialize( + jsonFull, + Rest.WeaviateRestClient.RestJsonSerializerOptions + ) + ); + var typed = Assert.IsType(roundTripped); + Assert.Equal("deepseek-chat", typed.Model); + Assert.Equal(0.7, typed.Temperature); + Assert.Equal(2048, typed.MaxTokens); + Assert.Equal(0.5, typed.FrequencyPenalty); + Assert.Equal(0.25, typed.PresencePenalty); + Assert.Equal(0.9, typed.TopP); + Assert.Equal("https://api.deepseek.com", typed.BaseURL); + Assert.NotNull(typed.Stop); + Assert.Equal(["\n\n"], typed.Stop); + } + /// /// Tests that collection rerank deserializes into i reranker config /// diff --git a/src/Weaviate.Client.Tests/Unit/TestGenerativeShortcuts.cs b/src/Weaviate.Client.Tests/Unit/TestGenerativeShortcuts.cs index 67ce6621..b3ab293e 100644 --- a/src/Weaviate.Client.Tests/Unit/TestGenerativeShortcuts.cs +++ b/src/Weaviate.Client.Tests/Unit/TestGenerativeShortcuts.cs @@ -310,6 +310,89 @@ await client Assert.Equal(0.5f, providerQuery.Mistral.Temperature); } + /// + /// Tests that a runtime DeepSeek provider reaches the request as GenerativeDeepseek with + /// every setting mapped to its own proto field. Unlike the collection config, the runtime + /// provider travels over gRPC, so this also pins that the vendored generative.proto carries + /// the deepseek member of the provider oneof. + /// + [Fact] + public async Task GenerateClient_FetchObjects_WithDeepseekProvider_MapsAllFields() + { + // Arrange + var provider = new Providers.Deepseek + { + BaseUrl = "https://api.deepseek.com", + Model = "deepseek-chat", + Temperature = 0.7, + MaxTokens = 2048, + FrequencyPenalty = 0.5, + PresencePenalty = 0.25, + TopP = 0.9, + Stop = ["\n\n"], + }; + var (client, getCapturedRequest) = CreateClientWithRequestCapture(); + + // Act + await client + .Collections.Use("TestCollection") + .Generate.FetchObjects( + limit: 10, + singlePrompt: "Summarize this", + provider: provider, + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var capturedRequest = getCapturedRequest(); + Assert.NotNull(capturedRequest); + var providerQuery = capturedRequest!.Generative!.Single!.Queries[0]; + Assert.NotNull(providerQuery.Deepseek); + Assert.Equal("https://api.deepseek.com", providerQuery.Deepseek.BaseUrl); + Assert.Equal("deepseek-chat", providerQuery.Deepseek.Model); + Assert.Equal(0.7, providerQuery.Deepseek.Temperature); + Assert.Equal(2048, providerQuery.Deepseek.MaxTokens); + Assert.Equal(0.5, providerQuery.Deepseek.FrequencyPenalty); + Assert.Equal(0.25, providerQuery.Deepseek.PresencePenalty); + Assert.Equal(0.9, providerQuery.Deepseek.TopP); + Assert.Equal(["\n\n"], providerQuery.Deepseek.Stop.Values); + } + + /// + /// Tests that an unset optional stays unset rather than travelling as a proto default, so + /// the server applies its own default instead of a client-invented zero. + /// + [Fact] + public async Task GenerateClient_FetchObjects_WithBareDeepseekProvider_LeavesOptionalsUnset() + { + // Arrange + var provider = new Providers.Deepseek { Model = "deepseek-reasoner" }; + var (client, getCapturedRequest) = CreateClientWithRequestCapture(); + + // Act + await client + .Collections.Use("TestCollection") + .Generate.FetchObjects( + limit: 10, + singlePrompt: "Summarize this", + provider: provider, + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var capturedRequest = getCapturedRequest(); + Assert.NotNull(capturedRequest); + var providerQuery = capturedRequest!.Generative!.Single!.Queries[0]; + Assert.NotNull(providerQuery.Deepseek); + Assert.Equal("deepseek-reasoner", providerQuery.Deepseek.Model); + Assert.False(providerQuery.Deepseek.HasTemperature); + Assert.False(providerQuery.Deepseek.HasMaxTokens); + Assert.False(providerQuery.Deepseek.HasFrequencyPenalty); + Assert.False(providerQuery.Deepseek.HasPresencePenalty); + Assert.False(providerQuery.Deepseek.HasTopP); + Assert.Null(providerQuery.Deepseek.Stop); + } + #endregion #region Helper Methods diff --git a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs index 33a74b33..a0bf713d 100644 --- a/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs +++ b/src/Weaviate.Client.Tests/Unit/TestVectorizers.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization; using Weaviate.Client.Models; using Weaviate.Client.Models.Vectorizers; using Quantizers = Weaviate.Client.Models.VectorIndex.Quantizers; @@ -347,6 +348,10 @@ public void Test_Multi2VecGoogle_Serializes_AudioFields_WeightedFields() /// /// Tests that Multi2VecGoogleGemini maps each string-array modality to its own key, and /// that the unweighted overload emits no weights object. + /// Also pins the module name: there is no multi2vec-google-gemini module on any + /// server, so the Gemini factory must emit the multi2vec-google module (under its + /// multi2vec-palm wire name) and select Gemini with apiEndpoint instead — + /// and must send neither projectId nor location, which are Vertex-only. /// [Fact] [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -364,7 +369,8 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() imageFields: new[] { "image" }, textFields: new[] { "text" }, videoFields: new[] { "video" }, - audioFields: new[] { "audio" } + audioFields: new[] { "audio" }, + dimensions: 512 ) ); @@ -372,18 +378,30 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_StringArray() var dto = vc.Vectorizer?.ToDto() ?? default; var json = JsonSerializer.Serialize( dto, + // Mirrors WeaviateRestClient.RestJsonSerializerOptions, so what is absent here is + // absent on the wire. new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false, } ); // Assert + Assert.Contains("\"multi2vec-palm\"", json); + Assert.DoesNotContain("multi2vec-google-gemini", json); + Assert.Contains("\"apiEndpoint\":\"generativelanguage.googleapis.com\"", json); + // Vertex-only settings the Gemini API has no equivalent of; neither may be sent. + Assert.DoesNotContain("projectId", json); + Assert.DoesNotContain("location", json); Assert.Contains("\"imageFields\":[\"image\"]", json); Assert.Contains("\"textFields\":[\"text\"]", json); Assert.Contains("\"videoFields\":[\"video\"]", json); Assert.Contains("\"audioFields\":[\"audio\"]", json); + Assert.Contains("\"dimensions\":512", json); + // Inherited shipped API on Multi2VecGoogle, but this factory never sets it. + Assert.DoesNotContain("vectorizeClassName", json); Assert.DoesNotContain("\"weights\"", json); } @@ -422,14 +440,20 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields() var dto = vc.Vectorizer?.ToDto() ?? default; var json = JsonSerializer.Serialize( dto, + // Mirrors WeaviateRestClient.RestJsonSerializerOptions, so what is absent here is + // absent on the wire. new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false, } ); // Assert + Assert.Contains("\"multi2vec-palm\"", json); + Assert.DoesNotContain("multi2vec-google-gemini", json); + Assert.Contains("\"apiEndpoint\":\"generativelanguage.googleapis.com\"", json); Assert.Contains("\"imageFields\":[\"image\",\"thumbnail\"]", json); Assert.Contains("\"textFields\":[\"text\"]", json); Assert.Contains("\"videoFields\":[\"video\",\"clip\"]", json); @@ -439,6 +463,10 @@ public void Test_Multi2VecGoogleGemini_Serializes_AudioFields_WeightedFields() + "\"textFields\":[0.23],\"videoFields\":[0.33,0.34]}", json ); + // dimensions is left unset by this case, so the omit-when-null path stays covered; + // vectorizeClassName is never settable through this factory at all. + Assert.DoesNotContain("dimensions", json); + Assert.DoesNotContain("vectorizeClassName", json); Assert.DoesNotContain("depthFields", json); Assert.DoesNotContain("imuFields", json); Assert.DoesNotContain("thermalFields", json); diff --git a/src/Weaviate.Client/BackupClient.cs b/src/Weaviate.Client/BackupClient.cs index 30c36234..fb5ad779 100644 --- a/src/Weaviate.Client/BackupClient.cs +++ b/src/Weaviate.Client/BackupClient.cs @@ -56,6 +56,8 @@ public async Task Create( CancellationToken cancellationToken = default ) { + await EnsureIncrementalBackupSupported(request); + var restRequest = BuildBackupCreateRequest(request); var response = await _client.RestClient.BackupCreate( request.Backend.Provider, @@ -85,6 +87,43 @@ public async Task CreateSync( return await operation.WaitForCompletion(timeout, cancellationToken); } + /// + /// The version file-based incremental backups are documented against; the wire field itself + /// is older (1.34.18). Gate on the documented floor, as the python client does. + /// + private static readonly Version IncrementalBackupMinimumVersion = new(1, 37, 0); + + /// + /// Throws when the request asks for an + /// incremental backup and the connected server predates support for it. Gated on the field + /// rather than on the whole operation, because plain backups still work on older servers. + /// + /// The request + /// + /// Thrown when is set and the + /// connected server version is below 1.37.0. + /// + private async Task EnsureIncrementalBackupSupported(BackupCreateRequest request) + { + if (string.IsNullOrEmpty(request.IncrementalBaseBackupId)) + return; + + await _client.EnsureInitializedAsync(); + + var serverVersion = _client.WeaviateVersion; + if (serverVersion is null) + return; + + if (serverVersion < IncrementalBackupMinimumVersion) + { + throw new WeaviateVersionMismatchException( + nameof(BackupCreateRequest.IncrementalBaseBackupId), + IncrementalBackupMinimumVersion, + serverVersion + ); + } + } + /// /// Builds the backup create request using the specified request /// @@ -101,6 +140,7 @@ private Rest.Dto.BackupCreateRequest BuildBackupCreateRequest(BackupCreateReques Id = request.Id, Include = request.IncludeCollections?.ToList(), Exclude = request.ExcludeCollections?.ToList(), + Incremental_base_backup_id = request.IncrementalBaseBackupId, Config = new Rest.Dto.BackupConfig { Bucket = bucket, @@ -396,6 +436,7 @@ private static Backup ToModel(Rest.Dto.BackupCreateStatusResponse dto, BackupBac ) { Size = dto.Size, + IncrementalBaseBackupId = dto.Incremental_base_backup_id, }; /// @@ -448,5 +489,9 @@ private static Backup ToModelListItem(Rest.Dto.Anonymous3 dto) => dto.StartedAt, dto.CompletedAt, null - ); + ) + { + Size = dto.Size, + IncrementalBaseBackupId = dto.Incremental_base_backup_id, + }; } diff --git a/src/Weaviate.Client/Configure/GenerativeConfig.cs b/src/Weaviate.Client/Configure/GenerativeConfig.cs index 17ed0fad..9a87b4dd 100644 --- a/src/Weaviate.Client/Configure/GenerativeConfig.cs +++ b/src/Weaviate.Client/Configure/GenerativeConfig.cs @@ -201,6 +201,40 @@ public IGenerativeConfig Databricks( TopP = topP, }; + /// + /// Create a generative configuration for DeepSeek. + /// + /// The model to use. + /// The temperature. + /// The maximum number of tokens to generate. + /// The frequency penalty. + /// The presence penalty. + /// The top P. + /// The base URL. + /// The stop sequences. + /// A instance. + public IGenerativeConfig Deepseek( + string? model = null, + double? temperature = null, + int? maxTokens = null, + double? frequencyPenalty = null, + double? presencePenalty = null, + double? topP = null, + string? baseURL = null, + string[]? stop = null + ) => + new GenerativeConfig.Deepseek + { + Model = model, + Temperature = temperature, + MaxTokens = maxTokens, + FrequencyPenalty = frequencyPenalty, + PresencePenalty = presencePenalty, + TopP = topP, + BaseURL = baseURL, + Stop = stop, + }; + /// /// Create a generative configuration for FriendliAI. /// diff --git a/src/Weaviate.Client/Configure/GenerativeProvider.cs b/src/Weaviate.Client/Configure/GenerativeProvider.cs index f123210d..5d0b9d6c 100644 --- a/src/Weaviate.Client/Configure/GenerativeProvider.cs +++ b/src/Weaviate.Client/Configure/GenerativeProvider.cs @@ -316,6 +316,40 @@ public Providers.Databricks Databricks( TopP = topP, }; + /// + /// Creates a DeepSeek runtime provider configuration. + /// + /// The base url + /// The model + /// The temperature + /// The max tokens + /// The frequency penalty + /// The presence penalty + /// The top + /// The stop + /// The providers deepseek + public Providers.Deepseek Deepseek( + string? baseUrl = null, + string? model = null, + double? temperature = null, + long? maxTokens = null, + double? frequencyPenalty = null, + double? presencePenalty = null, + double? topP = null, + List? stop = null + ) => + new() + { + BaseUrl = baseUrl, + Model = model, + Temperature = temperature, + MaxTokens = maxTokens, + FrequencyPenalty = frequencyPenalty, + PresencePenalty = presencePenalty, + TopP = topP, + Stop = stop, + }; + /// /// Dummies this instance /// diff --git a/src/Weaviate.Client/Configure/VectorizerFactory.cs b/src/Weaviate.Client/Configure/VectorizerFactory.cs index bef93a37..0ad8282d 100644 --- a/src/Weaviate.Client/Configure/VectorizerFactory.cs +++ b/src/Weaviate.Client/Configure/VectorizerFactory.cs @@ -321,6 +321,7 @@ public VectorizerConfig Multi2VecBind( /// The model /// The dimensions /// The vectorize collection name + /// The api endpoint /// The vectorizer config public VectorizerConfig Multi2VecGoogle( string projectId, @@ -332,12 +333,14 @@ public VectorizerConfig Multi2VecGoogle( int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, - bool? vectorizeCollectionName = null + bool? vectorizeCollectionName = null, + string? apiEndpoint = null ) => new Multi2VecGoogle { ProjectId = projectId, Location = location, + ApiEndpoint = apiEndpoint, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), @@ -371,6 +374,7 @@ public VectorizerConfig Multi2VecGoogle( /// The model /// The dimensions /// The vectorize collection name + /// The api endpoint /// The vectorizer config public VectorizerConfig Multi2VecGoogle( string projectId, @@ -382,12 +386,14 @@ public VectorizerConfig Multi2VecGoogle( int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, - bool? vectorizeCollectionName = null + bool? vectorizeCollectionName = null, + string? apiEndpoint = null ) => new Multi2VecGoogle { ProjectId = projectId, Location = location, + ApiEndpoint = apiEndpoint, ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), @@ -399,7 +405,10 @@ public VectorizerConfig Multi2VecGoogle( }; /// - /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API) + /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API). + /// Emits the multi2vec-google module with the Gemini API endpoint; there is no + /// separate Gemini module, so no project id or location is sent. + /// Requires Weaviate server version 1.34.20 or later. /// /// The image fields /// The text fields @@ -408,6 +417,7 @@ public VectorizerConfig Multi2VecGoogle( /// The API endpoint /// The video interval seconds /// The model + /// The dimensions /// The vectorizer config public VectorizerConfig Multi2VecGoogleGemini( WeightedFields imageFields, @@ -416,17 +426,23 @@ public VectorizerConfig Multi2VecGoogleGemini( WeightedFields audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, - string? model = null + string? model = null, + int? dimensions = null ) => - new Multi2VecGoogleGemini + new Multi2VecGoogle { + // The Gemini API has no Vertex project or region; null keeps both off the wire. + ProjectId = null, + Location = null, ApiEndpoint = apiEndpoint ?? "generativelanguage.googleapis.com", ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), AudioFields = ModalityFields.OrNull(audioFields), VideoIntervalSeconds = videoIntervalSeconds, - Model = model, + ModelId = model, + Dimensions = dimensions, + VectorizeCollectionName = null, // Named arguments are mandatory here: FromWeightedFields declares seven optional // modalities in the order image, text, audio, depth, imu, thermal, video, so the // positional call this replaced filed video weights under audio and audio weights @@ -440,7 +456,10 @@ public VectorizerConfig Multi2VecGoogleGemini( }; /// - /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API) + /// Multi2Vec Google Gemini configuration (using Google AI Studio/Gemini API). + /// Emits the multi2vec-google module with the Gemini API endpoint; there is no + /// separate Gemini module, so no project id or location is sent. + /// Requires Weaviate server version 1.34.20 or later. /// /// The image fields /// The text fields @@ -449,6 +468,7 @@ public VectorizerConfig Multi2VecGoogleGemini( /// The API endpoint /// The video interval seconds /// The model + /// The dimensions /// The vectorizer config public VectorizerConfig Multi2VecGoogleGemini( string[]? imageFields = null, @@ -457,17 +477,23 @@ public VectorizerConfig Multi2VecGoogleGemini( string[]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, - string? model = null + string? model = null, + int? dimensions = null ) => - new Multi2VecGoogleGemini + new Multi2VecGoogle { + // The Gemini API has no Vertex project or region; null keeps both off the wire. + ProjectId = null, + Location = null, ApiEndpoint = apiEndpoint ?? "generativelanguage.googleapis.com", ImageFields = ModalityFields.OrNull(imageFields), TextFields = ModalityFields.OrNull(textFields), VideoFields = ModalityFields.OrNull(videoFields), AudioFields = ModalityFields.OrNull(audioFields), VideoIntervalSeconds = videoIntervalSeconds, - Model = model, + ModelId = model, + Dimensions = dimensions, + VectorizeCollectionName = null, }; /// diff --git a/src/Weaviate.Client/Models/Aggregate.cs b/src/Weaviate.Client/Models/Aggregate.cs index 23fe6bd1..afc41814 100644 --- a/src/Weaviate.Client/Models/Aggregate.cs +++ b/src/Weaviate.Client/Models/Aggregate.cs @@ -716,11 +716,12 @@ V1.AggregateReply.Types.Aggregations.Types.Aggregation x { return x.AggregationCase switch { + // Every scalar here is optional in aggregate.proto: unset maps to null, not 0/false. V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Text => (Aggregate.Property) new Aggregate.Text { - Count = x.Text.Count, + Count = x.Text.HasCount ? x.Text.Count : null, TopOccurrences = ( x.Text.TopOccurences is null ? [] @@ -736,38 +737,40 @@ x.Text.TopOccurences is null V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Int => new Aggregate.Integer { - Count = x.Int.Count, - Maximum = x.Int.Maximum, - Mean = x.Int.Mean, - Median = x.Int.Median, - Minimum = x.Int.Minimum, - Mode = x.Int.Mode, - Sum = x.Int.Sum, + Count = x.Int.HasCount ? x.Int.Count : null, + Maximum = x.Int.HasMaximum ? x.Int.Maximum : null, + Mean = x.Int.HasMean ? x.Int.Mean : null, + Median = x.Int.HasMedian ? x.Int.Median : null, + Minimum = x.Int.HasMinimum ? x.Int.Minimum : null, + Mode = x.Int.HasMode ? x.Int.Mode : null, + Sum = x.Int.HasSum ? x.Int.Sum : null, }, V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Number => new Aggregate.Number { - Count = x.Number.Count, - Maximum = x.Number.Maximum, - Mean = x.Number.Mean, - Median = x.Number.Median, - Minimum = x.Number.Minimum, - Mode = x.Number.Mode, - Sum = x.Number.Sum, + Count = x.Number.HasCount ? x.Number.Count : null, + Maximum = x.Number.HasMaximum ? x.Number.Maximum : null, + Mean = x.Number.HasMean ? x.Number.Mean : null, + Median = x.Number.HasMedian ? x.Number.Median : null, + Minimum = x.Number.HasMinimum ? x.Number.Minimum : null, + Mode = x.Number.HasMode ? x.Number.Mode : null, + Sum = x.Number.HasSum ? x.Number.Sum : null, }, V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Boolean => new Aggregate.Boolean { - Count = x.Boolean.Count, - PercentageFalse = x.Boolean.PercentageFalse, - PercentageTrue = x.Boolean.PercentageTrue, - TotalFalse = x.Boolean.TotalFalse, - TotalTrue = x.Boolean.TotalTrue, + Count = x.Boolean.HasCount ? x.Boolean.Count : null, + PercentageFalse = x.Boolean.HasPercentageFalse + ? x.Boolean.PercentageFalse + : null, + PercentageTrue = x.Boolean.HasPercentageTrue ? x.Boolean.PercentageTrue : null, + TotalFalse = x.Boolean.HasTotalFalse ? x.Boolean.TotalFalse : null, + TotalTrue = x.Boolean.HasTotalTrue ? x.Boolean.TotalTrue : null, }, V1.AggregateReply.Types.Aggregations.Types.Aggregation.AggregationOneofCase.Date => new Aggregate.Date { - Count = x.Date.Count, + Count = x.Date.HasCount ? x.Date.Count : null, Maximum = x.Date.HasMaximum ? DateTime.Parse( x.Date.Maximum, @@ -1269,24 +1272,24 @@ public record Number : Numeric { }; public record Boolean : Property { /// - /// Gets or sets the value of the percentage false + /// Gets or sets the value of the percentage false, or null when the server did not send the value. /// - public double PercentageFalse { get; internal set; } + public double? PercentageFalse { get; internal set; } /// - /// Gets or sets the value of the percentage true + /// Gets or sets the value of the percentage true, or null when the server did not send the value. /// - public double PercentageTrue { get; internal set; } + public double? PercentageTrue { get; internal set; } /// - /// Gets or sets the value of the total false + /// Gets or sets the value of the total false, or null when the server did not send the value. /// - public long TotalFalse { get; internal set; } + public long? TotalFalse { get; internal set; } /// - /// Gets or sets the value of the total true + /// Gets or sets the value of the total true, or null when the server did not send the value. /// - public long TotalTrue { get; internal set; } + public long? TotalTrue { get; internal set; } }; /// diff --git a/src/Weaviate.Client/Models/Backup.cs b/src/Weaviate.Client/Models/Backup.cs index 95adcb39..cb7abe05 100644 --- a/src/Weaviate.Client/Models/Backup.cs +++ b/src/Weaviate.Client/Models/Backup.cs @@ -295,18 +295,36 @@ public record Backup( /// Gets the size of the backup in GiB. Available after completion. /// public double? Size { get; init; } + + /// + /// Gets the id of the base backup this backup was built on. Only servers 1.37.6 and later + /// return it; on older servers null does not mean the backup is non-incremental. + /// + public string? IncrementalBaseBackupId { get; init; } } /// /// Options for creating a backup /// +/// The identifier of the backup to create. +/// The storage backend to write the backup to. +/// The collections to include; all when null. +/// The collections to exclude; none when null. +/// The share of CPU the backup may use. +/// The compression level to write with. +/// +/// The identifier of an existing backup to use as the base for a file-based incremental backup. +/// Files unchanged since the base backup are not copied again and are restored from the base. +/// Requires Weaviate server version 1.37.0 or later. +/// public record BackupCreateRequest( string Id, BackupBackend Backend, AutoArray? IncludeCollections = null, AutoArray? ExcludeCollections = null, int? CPUPercentage = null, - BackupCompressionLevel? CompressionLevel = null + BackupCompressionLevel? CompressionLevel = null, + string? IncrementalBaseBackupId = null ); /// diff --git a/src/Weaviate.Client/Models/Generative/Providers.cs b/src/Weaviate.Client/Models/Generative/Providers.cs index 2c23cd1d..abab1bae 100644 --- a/src/Weaviate.Client/Models/Generative/Providers.cs +++ b/src/Weaviate.Client/Models/Generative/Providers.cs @@ -822,6 +822,52 @@ public record Databricks() : GenerativeProvider("databricks") public double? TopP { get; set; } } + /// + /// Configuration for DeepSeek generative AI provider. + /// + public record Deepseek() : GenerativeProvider("deepseek") + { + /// + /// Gets or sets the base URL for the DeepSeek API endpoint. + /// + public string? BaseUrl { get; set; } + + /// + /// Gets or sets the model identifier to use. + /// + public string? Model { get; set; } + + /// + /// Gets or sets the temperature for controlling randomness in generation. + /// + public double? Temperature { get; set; } + + /// + /// Gets or sets the maximum number of tokens to generate. + /// + public long? MaxTokens { get; set; } + + /// + /// Gets or sets the frequency penalty to reduce repetition. + /// + public double? FrequencyPenalty { get; set; } + + /// + /// Gets or sets the presence penalty to encourage topic diversity. + /// + public double? PresencePenalty { get; set; } + + /// + /// Gets or sets the top-p (nucleus) sampling parameter. + /// + public double? TopP { get; set; } + + /// + /// Gets or sets the sequences where generation should stop. + /// + public List? Stop { get; set; } + } + /// /// Configuration for FriendliAI generative AI provider. /// diff --git a/src/Weaviate.Client/Models/GenerativeConfig.cs b/src/Weaviate.Client/Models/GenerativeConfig.cs index 85d6858a..c8654564 100644 --- a/src/Weaviate.Client/Models/GenerativeConfig.cs +++ b/src/Weaviate.Client/Models/GenerativeConfig.cs @@ -378,6 +378,68 @@ internal Databricks() { } public double? TopP { get; set; } } + /// + /// Configuration for DeepSeek generative AI provider. + /// + public record Deepseek : IGenerativeConfig + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + internal Deepseek() { } + + /// + /// The type value for DeepSeek configuration. + /// + public const string TypeValue = "generative-deepseek"; + + /// + /// Gets the type identifier for the configuration. + /// + public string Type => TypeValue; + + /// + /// Gets or sets the model identifier to use. + /// + public string? Model { get; set; } + + /// + /// Gets or sets the temperature for controlling randomness in generation. + /// + public double? Temperature { get; set; } + + /// + /// Gets or sets the maximum number of tokens to generate. + /// + public int? MaxTokens { get; set; } + + /// + /// Gets or sets the frequency penalty to reduce repetition. + /// + public double? FrequencyPenalty { get; set; } + + /// + /// Gets or sets the presence penalty to encourage topic diversity. + /// + public double? PresencePenalty { get; set; } + + /// + /// Gets or sets the top-p (nucleus) sampling parameter. + /// + public double? TopP { get; set; } + + /// + /// Gets or sets the base URL for the DeepSeek API endpoint. + /// + public string? BaseURL { get; set; } + + /// + /// Gets or sets the sequences where generation should stop. + /// + public string[]? Stop { get; set; } + } + /// /// Configuration for FriendliAI generative AI provider. /// diff --git a/src/Weaviate.Client/Models/Serialization.GenerativeConfig.cs b/src/Weaviate.Client/Models/Serialization.GenerativeConfig.cs index 1bb515c0..b8653376 100644 --- a/src/Weaviate.Client/Models/Serialization.GenerativeConfig.cs +++ b/src/Weaviate.Client/Models/Serialization.GenerativeConfig.cs @@ -48,6 +48,11 @@ internal static class GenerativeConfigSerialization text, Rest.WeaviateRestClient.RestJsonSerializerOptions ), + GenerativeConfig.Deepseek.TypeValue => + JsonSerializer.Deserialize( + text, + Rest.WeaviateRestClient.RestJsonSerializerOptions + ), GenerativeConfig.FriendliAI.TypeValue => JsonSerializer.Deserialize( text, diff --git a/src/Weaviate.Client/Models/Typed/TypedAggregateResults.cs b/src/Weaviate.Client/Models/Typed/TypedAggregateResults.cs index 308d42b2..a8cab359 100644 --- a/src/Weaviate.Client/Models/Typed/TypedAggregateResults.cs +++ b/src/Weaviate.Client/Models/Typed/TypedAggregateResults.cs @@ -397,38 +397,6 @@ Aggregate.Text t _ => null, }; } - - /// - /// Converts a long value to the target type. - /// - private static object? ConvertToNumeric(long value, Type targetType) - { - return targetType switch - { - _ when targetType == typeof(long) => value, - _ when targetType == typeof(int) => (int)value, - _ when targetType == typeof(double) => (double)value, - _ when targetType == typeof(float) => (float)value, - _ when targetType == typeof(decimal) => (decimal)value, - _ => null, - }; - } - - /// - /// Converts a double value to the target type. - /// - private static object? ConvertToNumeric(double value, Type targetType) - { - return targetType switch - { - _ when targetType == typeof(double) => value, - _ when targetType == typeof(float) => (float)value, - _ when targetType == typeof(decimal) => (decimal)value, - _ when targetType == typeof(long) => (long)value, - _ when targetType == typeof(int) => (int)value, - _ => null, - }; - } } /// diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs index cf66c1bf..af202ec9 100644 --- a/src/Weaviate.Client/Models/Vectorizer.cs +++ b/src/Weaviate.Client/Models/Vectorizer.cs @@ -380,14 +380,25 @@ public record Multi2VecGoogle : VectorizerConfig internal Multi2VecGoogle() { } /// - /// Gets or sets the value of the project id + /// Gets or sets the Google Cloud project id. + /// Required by Vertex AI; omitted when null, as it is for the Gemini + /// (generative-language) API, which is not scoped to a project. /// - public required string ProjectId { get; set; } + public string? ProjectId { get; set; } = null; /// - /// Gets or sets the value of the location + /// Gets or sets the Google Vertex AI region. + /// Required by Vertex AI; omitted when null, as it is for the Gemini + /// (generative-language) API, which has no region. /// - public required string Location { get; set; } + public string? Location { get; set; } = null; + + /// + /// Gets or sets the value of the api endpoint. + /// Set to generativelanguage.googleapis.com to call the Gemini API instead of + /// Vertex AI; when omitted the server applies its default. + /// + public string? ApiEndpoint { get; set; } = null; /// /// Gets or sets the value of the image fields @@ -452,61 +463,16 @@ internal Multi2VecPalm() { } } /// - /// The configuration for multi-media vectorization using the Google Gemini module. - /// See the documentation for detailed usage. + /// Deprecated. Use instead. /// - [Vectorizer("multi2vec-google-gemini")] - public record Multi2VecGoogleGemini : VectorizerConfig + [Obsolete("Use Multi2VecGoogle with ApiEndpoint = 'generativelanguage.googleapis.com'.")] + public record Multi2VecGoogleGemini : Multi2VecGoogle { /// /// Initializes a new instance of the class /// [JsonConstructor] internal Multi2VecGoogleGemini() { } - - /// - /// Gets or sets the value of the api endpoint - /// - public string? ApiEndpoint { get; set; } = "generativelanguage.googleapis.com"; - - /// - /// Gets or sets the value of the image fields - /// - public string[]? ImageFields { get; set; } = null; - - /// - /// Gets or sets the value of the text fields - /// - public string[]? TextFields { get; set; } = null; - - /// - /// Gets or sets the value of the video fields - /// - public string[]? VideoFields { get; set; } = null; - - /// - /// Gets or sets the value of the audio fields - /// - public string[]? AudioFields { get; set; } = null; - - /// - /// Gets or sets the value of the video interval seconds - /// - public int? VideoIntervalSeconds { get; set; } = null; - - /// - /// Gets or sets the value of the model - /// - [JsonPropertyName("modelId")] - public string? Model { get; set; } = null; - - /// - /// Gets or sets the per-modality weights (the weights object), omitted when null. - /// [JsonInclude] is required: the serializer skips internal properties by default. - /// - [JsonInclude] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - internal VectorizerWeights? Weights { get; set; } = null; } /// diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index b9247064..bd29b2d6 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -343,3 +343,109 @@ static Weaviate.Client.Typed.TypedGenerateClientHybridExtensions.Hybrid(this static Weaviate.Client.Typed.TypedGenerateClientHybridExtensions.Hybrid(this Weaviate.Client.Typed.TypedGenerateClient! client, string! query, Weaviate.Client.Models.HybridVectorInput.FactoryFn! vectors, float? alpha = null, string![]? queryProperties = null, Weaviate.Client.Models.HybridFusion? fusionType = null, float? maxVectorDistance = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.BM25Operator? bm25Operator = null, Weaviate.Client.Models.Diversity? diversitySelection = null, uint? autoLimit = null, Weaviate.Client.Models.Filter? filters = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.Boost? boost = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, System.Collections.Generic.IList? returnReferences = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! static Weaviate.Client.Typed.TypedQueryClientHybridExtensions.Hybrid(this Weaviate.Client.Typed.TypedQueryClient! client, string! query, Weaviate.Client.Models.HybridVectorInput.FactoryFn! vectors, Weaviate.Client.Models.GroupByRequest! groupBy, float? alpha = null, string![]? queryProperties = null, Weaviate.Client.Models.HybridFusion? fusionType = null, float? maxVectorDistance = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.BM25Operator? bm25Operator = null, Weaviate.Client.Models.Diversity? diversitySelection = null, uint? autoLimit = null, Weaviate.Client.Models.Filter? filters = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.Boost? boost = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, System.Collections.Generic.IList? returnReferences = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! static Weaviate.Client.Typed.TypedQueryClientHybridExtensions.Hybrid(this Weaviate.Client.Typed.TypedQueryClient! client, string! query, Weaviate.Client.Models.HybridVectorInput.FactoryFn! vectors, float? alpha = null, string![]? queryProperties = null, Weaviate.Client.Models.HybridFusion? fusionType = null, float? maxVectorDistance = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.BM25Operator? bm25Operator = null, Weaviate.Client.Models.Diversity? diversitySelection = null, uint? autoLimit = null, Weaviate.Client.Models.Filter? filters = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.Boost? boost = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, System.Collections.Generic.IList? returnReferences = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.Location.get -> string! +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ProjectId.get -> string! +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.ApiEndpoint.get -> string? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.ApiEndpoint.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.AudioFields.get -> string![]? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.AudioFields.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.ImageFields.get -> string![]? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.ImageFields.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.Model.get -> string? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.Model.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.TextFields.get -> string![]? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.TextFields.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.VideoFields.get -> string![]? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.VideoFields.set -> void +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.VideoIntervalSeconds.get -> int? +*REMOVED*Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.VideoIntervalSeconds.set -> void +*REMOVED*override sealed Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.Equals(Weaviate.Client.Models.VectorizerConfig? other) -> bool +*REMOVED*Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null) -> Weaviate.Client.Models.VectorizerConfig! +override sealed Weaviate.Client.Models.Vectorizer.Multi2VecGoogleGemini.Equals(Weaviate.Client.Models.Vectorizer.Multi2VecGoogle? other) -> bool +Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ApiEndpoint.get -> string? +Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ApiEndpoint.set -> void +Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.Location.get -> string? +Weaviate.Client.Models.Vectorizer.Multi2VecGoogle.ProjectId.get -> string? +Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null, string? apiEndpoint = null) -> Weaviate.Client.Models.VectorizerConfig! +Weaviate.Client.VectorizerFactory.Multi2VecGoogle(string! projectId, string! location, Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null, bool? vectorizeCollectionName = null, string? apiEndpoint = null) -> Weaviate.Client.Models.VectorizerConfig! +Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(string![]? imageFields = null, string![]? textFields = null, string![]? videoFields = null, string![]? audioFields = null, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null) -> Weaviate.Client.Models.VectorizerConfig! +Weaviate.Client.VectorizerFactory.Multi2VecGoogleGemini(Weaviate.Client.Models.WeightedFields! imageFields, Weaviate.Client.Models.WeightedFields! textFields, Weaviate.Client.Models.WeightedFields! videoFields, Weaviate.Client.Models.WeightedFields! audioFields, string? apiEndpoint = null, int? videoIntervalSeconds = null, string? model = null, int? dimensions = null) -> Weaviate.Client.Models.VectorizerConfig! +*REMOVED*Weaviate.Client.Models.BackupCreateRequest.BackupCreateRequest(string! Id, Weaviate.Client.Models.BackupBackend! Backend, Weaviate.Client.Internal.AutoArray? IncludeCollections = null, Weaviate.Client.Internal.AutoArray? ExcludeCollections = null, int? CPUPercentage = null, Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel = null) -> void +*REMOVED*Weaviate.Client.Models.BackupCreateRequest.Deconstruct(out string! Id, out Weaviate.Client.Models.BackupBackend! Backend, out Weaviate.Client.Internal.AutoArray? IncludeCollections, out Weaviate.Client.Internal.AutoArray? ExcludeCollections, out int? CPUPercentage, out Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel) -> void +Weaviate.Client.Models.BackupCreateRequest.BackupCreateRequest(string! Id, Weaviate.Client.Models.BackupBackend! Backend, Weaviate.Client.Internal.AutoArray? IncludeCollections = null, Weaviate.Client.Internal.AutoArray? ExcludeCollections = null, int? CPUPercentage = null, Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel = null, string? IncrementalBaseBackupId = null) -> void +Weaviate.Client.Models.BackupCreateRequest.Deconstruct(out string! Id, out Weaviate.Client.Models.BackupBackend! Backend, out Weaviate.Client.Internal.AutoArray? IncludeCollections, out Weaviate.Client.Internal.AutoArray? ExcludeCollections, out int? CPUPercentage, out Weaviate.Client.Models.BackupCompressionLevel? CompressionLevel, out string? IncrementalBaseBackupId) -> void +Weaviate.Client.Models.BackupCreateRequest.IncrementalBaseBackupId.get -> string? +Weaviate.Client.Models.BackupCreateRequest.IncrementalBaseBackupId.init -> void +Weaviate.Client.Models.Backup.IncrementalBaseBackupId.get -> string? +Weaviate.Client.Models.Backup.IncrementalBaseBackupId.init -> void +const Weaviate.Client.Models.GenerativeConfig.Deepseek.TypeValue = "generative-deepseek" -> string! +override sealed Weaviate.Client.Models.Generative.Providers.Deepseek.Equals(Weaviate.Client.Models.GenerativeProvider? other) -> bool +override Weaviate.Client.Models.Generative.Providers.Deepseek.$() -> Weaviate.Client.Models.Generative.Providers.Deepseek! +override Weaviate.Client.Models.Generative.Providers.Deepseek.EqualityContract.get -> System.Type! +override Weaviate.Client.Models.Generative.Providers.Deepseek.Equals(object? obj) -> bool +override Weaviate.Client.Models.Generative.Providers.Deepseek.GetHashCode() -> int +override Weaviate.Client.Models.Generative.Providers.Deepseek.PrintMembers(System.Text.StringBuilder! builder) -> bool +override Weaviate.Client.Models.Generative.Providers.Deepseek.ToString() -> string! +override Weaviate.Client.Models.GenerativeConfig.Deepseek.Equals(object? obj) -> bool +override Weaviate.Client.Models.GenerativeConfig.Deepseek.GetHashCode() -> int +override Weaviate.Client.Models.GenerativeConfig.Deepseek.ToString() -> string! +static Weaviate.Client.Models.Generative.Providers.Deepseek.operator !=(Weaviate.Client.Models.Generative.Providers.Deepseek? left, Weaviate.Client.Models.Generative.Providers.Deepseek? right) -> bool +static Weaviate.Client.Models.Generative.Providers.Deepseek.operator ==(Weaviate.Client.Models.Generative.Providers.Deepseek? left, Weaviate.Client.Models.Generative.Providers.Deepseek? right) -> bool +static Weaviate.Client.Models.GenerativeConfig.Deepseek.operator !=(Weaviate.Client.Models.GenerativeConfig.Deepseek? left, Weaviate.Client.Models.GenerativeConfig.Deepseek? right) -> bool +static Weaviate.Client.Models.GenerativeConfig.Deepseek.operator ==(Weaviate.Client.Models.GenerativeConfig.Deepseek? left, Weaviate.Client.Models.GenerativeConfig.Deepseek? right) -> bool +virtual Weaviate.Client.Models.Generative.Providers.Deepseek.Equals(Weaviate.Client.Models.Generative.Providers.Deepseek? other) -> bool +virtual Weaviate.Client.Models.GenerativeConfig.Deepseek.$() -> Weaviate.Client.Models.GenerativeConfig.Deepseek! +virtual Weaviate.Client.Models.GenerativeConfig.Deepseek.EqualityContract.get -> System.Type! +virtual Weaviate.Client.Models.GenerativeConfig.Deepseek.Equals(Weaviate.Client.Models.GenerativeConfig.Deepseek? other) -> bool +virtual Weaviate.Client.Models.GenerativeConfig.Deepseek.PrintMembers(System.Text.StringBuilder! builder) -> bool +Weaviate.Client.GenerativeConfigFactory.Deepseek(string? model = null, double? temperature = null, int? maxTokens = null, double? frequencyPenalty = null, double? presencePenalty = null, double? topP = null, string? baseURL = null, string![]? stop = null) -> Weaviate.Client.Models.IGenerativeConfig! +Weaviate.Client.GenerativeProviderFactory.Deepseek(string? baseUrl = null, string? model = null, double? temperature = null, long? maxTokens = null, double? frequencyPenalty = null, double? presencePenalty = null, double? topP = null, System.Collections.Generic.List? stop = null) -> Weaviate.Client.Models.Generative.Providers.Deepseek! +Weaviate.Client.Models.Generative.Providers.Deepseek +Weaviate.Client.Models.Generative.Providers.Deepseek.BaseUrl.get -> string? +Weaviate.Client.Models.Generative.Providers.Deepseek.BaseUrl.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Deepseek() -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Deepseek(Weaviate.Client.Models.Generative.Providers.Deepseek! original) -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.FrequencyPenalty.get -> double? +Weaviate.Client.Models.Generative.Providers.Deepseek.FrequencyPenalty.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.MaxTokens.get -> long? +Weaviate.Client.Models.Generative.Providers.Deepseek.MaxTokens.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Model.get -> string? +Weaviate.Client.Models.Generative.Providers.Deepseek.Model.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.PresencePenalty.get -> double? +Weaviate.Client.Models.Generative.Providers.Deepseek.PresencePenalty.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Stop.get -> System.Collections.Generic.List? +Weaviate.Client.Models.Generative.Providers.Deepseek.Stop.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.Temperature.get -> double? +Weaviate.Client.Models.Generative.Providers.Deepseek.Temperature.set -> void +Weaviate.Client.Models.Generative.Providers.Deepseek.TopP.get -> double? +Weaviate.Client.Models.Generative.Providers.Deepseek.TopP.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek +Weaviate.Client.Models.GenerativeConfig.Deepseek.BaseURL.get -> string? +Weaviate.Client.Models.GenerativeConfig.Deepseek.BaseURL.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Deepseek(Weaviate.Client.Models.GenerativeConfig.Deepseek! original) -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.FrequencyPenalty.get -> double? +Weaviate.Client.Models.GenerativeConfig.Deepseek.FrequencyPenalty.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.MaxTokens.get -> int? +Weaviate.Client.Models.GenerativeConfig.Deepseek.MaxTokens.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Model.get -> string? +Weaviate.Client.Models.GenerativeConfig.Deepseek.Model.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.PresencePenalty.get -> double? +Weaviate.Client.Models.GenerativeConfig.Deepseek.PresencePenalty.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Stop.get -> string![]? +Weaviate.Client.Models.GenerativeConfig.Deepseek.Stop.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Temperature.get -> double? +Weaviate.Client.Models.GenerativeConfig.Deepseek.Temperature.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.TopP.get -> double? +Weaviate.Client.Models.GenerativeConfig.Deepseek.TopP.set -> void +Weaviate.Client.Models.GenerativeConfig.Deepseek.Type.get -> string! +*REMOVED*Weaviate.Client.Models.Aggregate.Boolean.PercentageFalse.get -> double +*REMOVED*Weaviate.Client.Models.Aggregate.Boolean.PercentageTrue.get -> double +*REMOVED*Weaviate.Client.Models.Aggregate.Boolean.TotalFalse.get -> long +*REMOVED*Weaviate.Client.Models.Aggregate.Boolean.TotalTrue.get -> long +Weaviate.Client.Models.Aggregate.Boolean.PercentageFalse.get -> double? +Weaviate.Client.Models.Aggregate.Boolean.PercentageTrue.get -> double? +Weaviate.Client.Models.Aggregate.Boolean.TotalFalse.get -> long? +Weaviate.Client.Models.Aggregate.Boolean.TotalTrue.get -> long? diff --git a/src/Weaviate.Client/gRPC/Search.Builders.cs b/src/Weaviate.Client/gRPC/Search.Builders.cs index b93c64ce..7afdc9d5 100644 --- a/src/Weaviate.Client/gRPC/Search.Builders.cs +++ b/src/Weaviate.Client/gRPC/Search.Builders.cs @@ -541,6 +541,19 @@ private static V1.GenerativeProvider GetGenerativeProvider(GenerativeProvider pr SetIfNotNull(v => result.Databricks.Temperature = (float)v, a.Temperature); SetIfNotNull(v => result.Databricks.TopP = (float)v, a.TopP); break; + case Models.Generative.Providers.Deepseek a: + result.Deepseek = new V1.GenerativeDeepseek + { + BaseUrl = a.BaseUrl ?? string.Empty, + Model = a.Model ?? string.Empty, + Stop = a.Stop != null ? new V1.TextArray { Values = { a.Stop } } : null, + }; + SetIfNotNull(v => result.Deepseek.Temperature = v, a.Temperature); + SetIfNotNull(v => result.Deepseek.MaxTokens = v, a.MaxTokens); + SetIfNotNull(v => result.Deepseek.FrequencyPenalty = v, a.FrequencyPenalty); + SetIfNotNull(v => result.Deepseek.PresencePenalty = v, a.PresencePenalty); + SetIfNotNull(v => result.Deepseek.TopP = v, a.TopP); + break; case Models.Generative.Providers.FriendliAI a: result.Friendliai = new V1.GenerativeFriendliAI { diff --git a/src/Weaviate.Client/gRPC/proto/v1/generative.proto b/src/Weaviate.Client/gRPC/proto/v1/generative.proto index 4bb98095..18da4598 100644 --- a/src/Weaviate.Client/gRPC/proto/v1/generative.proto +++ b/src/Weaviate.Client/gRPC/proto/v1/generative.proto @@ -49,6 +49,7 @@ message GenerativeProvider { GenerativeNvidia nvidia = 13; GenerativeXAI xai = 14; GenerativeContextualAI contextualai = 15; + GenerativeDeepseek deepseek = 16; } } @@ -219,6 +220,17 @@ message GenerativeContextualAI{ optional TextArray knowledge = 7; } +message GenerativeDeepseek{ + optional string base_url = 1; + optional string model = 2; + optional double temperature = 3; + optional int64 max_tokens = 4; + optional double frequency_penalty = 5; + optional double presence_penalty = 6; + optional double top_p = 7; + optional TextArray stop = 8; +} + message GenerativeAnthropicMetadata { message Usage { int64 input_tokens = 1; @@ -336,6 +348,15 @@ message GenerativeXAIMetadata { optional Usage usage = 1; } +message GenerativeDeepseekMetadata { + message Usage { + optional int64 prompt_tokens = 1; + optional int64 completion_tokens = 2; + optional int64 total_tokens = 3; + } + optional Usage usage = 1; +} + message GenerativeMetadata { oneof kind { GenerativeAnthropicMetadata anthropic = 1; @@ -351,6 +372,7 @@ message GenerativeMetadata { GenerativeFriendliAIMetadata friendliai = 11; GenerativeNvidiaMetadata nvidia = 12; GenerativeXAIMetadata xai = 13; + GenerativeDeepseekMetadata deepseek = 14; } }