diff --git a/DashScope.net.sln b/DashScope.net.sln index 760dd0f..97f305d 100644 --- a/DashScope.net.sln +++ b/DashScope.net.sln @@ -16,6 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props + .github\workflows\nuget-build.yml = .github\workflows\nuget-build.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DashScope.UnitTests", "tests\DashScope.UnitTests\DashScope.UnitTests.csproj", "{837CD31F-A2EA-4379-B325-3A20285F7577}" @@ -38,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DashVector.UnitTests", "tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DashVector.Sample", "samples\DashVector.Sample\DashVector.Sample.csproj", "{0B09CBA0-491C-49B5-A498-940AEB7210DF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DashVector.SemanticKernel", "src\DashVector.SemanticKernel\DashVector.SemanticKernel.csproj", "{648ABFAD-C62C-4D58-B968-91F6BEF0831D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +87,10 @@ Global {0B09CBA0-491C-49B5-A498-940AEB7210DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {0B09CBA0-491C-49B5-A498-940AEB7210DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {0B09CBA0-491C-49B5-A498-940AEB7210DF}.Release|Any CPU.Build.0 = Release|Any CPU + {648ABFAD-C62C-4D58-B968-91F6BEF0831D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {648ABFAD-C62C-4D58-B968-91F6BEF0831D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {648ABFAD-C62C-4D58-B968-91F6BEF0831D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {648ABFAD-C62C-4D58-B968-91F6BEF0831D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -99,6 +106,7 @@ Global {580E981D-49BB-4C34-B4BA-2A1C85B6016D} = {BB7B834A-464F-4F81-A649-E63AA0C53C24} {FFF43DC0-8591-4817-ABEF-C11598BE6D24} = {ED489DAA-351A-43F9-A3DF-7DBEBF7677CD} {0B09CBA0-491C-49B5-A498-940AEB7210DF} = {2FDFB5A9-FE06-4822-933E-5D1C6D9160C9} + {648ABFAD-C62C-4D58-B968-91F6BEF0831D} = {BB7B834A-464F-4F81-A649-E63AA0C53C24} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4DB8E739-7470-4879-9DCE-E9BACBA58715} diff --git a/Directory.Packages.props b/Directory.Packages.props index b66aa68..d4c9863 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,7 @@ + diff --git a/samples/SK-DashScope.Sample/Controllers/ApiController.cs b/samples/SK-DashScope.Sample/Controllers/ApiController.cs index 38763e2..e31b709 100644 --- a/samples/SK-DashScope.Sample/Controllers/ApiController.cs +++ b/samples/SK-DashScope.Sample/Controllers/ApiController.cs @@ -6,6 +6,7 @@ using System.Text; using DashScope.SemanticKernel; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.Memory; namespace SK_DashScope.Sample.Controllers { @@ -14,10 +15,13 @@ namespace SK_DashScope.Sample.Controllers public class ApiController : ControllerBase { private readonly Kernel kernel; + private readonly ISemanticTextMemory memory; + const string CollectionName = "DashScopeMemoryStore"; - public ApiController(Kernel kernel) + public ApiController(Kernel kernel, ISemanticTextMemory memory) { this.kernel = kernel; + this.memory = memory; } [HttpPost] @@ -151,5 +155,49 @@ public async Task SemanticAsync([FromBody] UserInput input, Cance return Ok(new { value, usage }); } + + [HttpPost("save_memory")] + public async Task SaveMemory([FromBody] UserInput input, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(input.Text)) + { + return string.Empty; + } + var id = Guid.NewGuid().ToString("N"); + + return await memory.SaveInformationAsync(CollectionName, input.Text, id, cancellationToken: cancellationToken); + } + + [HttpPost("get_memory")] + public async Task GetMemory([FromBody] UserInput input, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(input.Text)) + { + return null; + } + + var id = input.Text; + return await memory.GetAsync(CollectionName, id, true, cancellationToken: cancellationToken); + } + + [HttpPost("query_memory")] + public async Task> QueryMemory([FromBody] UserInput input, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(input.Text)) + { + return Array.Empty(); + } + + var query = input.Text; + var records = memory.SearchAsync(CollectionName, query, 10, 0, true, cancellationToken: cancellationToken); + + var results = new List(); + await foreach (var record in records) + { + results.Add(record); + } + + return results; + } } } \ No newline at end of file diff --git a/samples/SK-DashScope.Sample/Program.cs b/samples/SK-DashScope.Sample/Program.cs index 977c987..e85002d 100644 --- a/samples/SK-DashScope.Sample/Program.cs +++ b/samples/SK-DashScope.Sample/Program.cs @@ -1,3 +1,5 @@ +using DashVector; +using DashVector.SemanticKernel; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Memory; @@ -23,6 +25,15 @@ { return new MemoryBuilder() .WithDashScopeTextEmbeddingGenerationService(builder.Configuration["DashScope:ApiKey"]!) + .WithMemoryStore(factory => + { + var dashVectorClient = new DashVectorClient(builder.Configuration["DashVector:ApiKey"]!, + builder.Configuration["DashVector:Endpoint"]!); + return new DashVectorMemoryStore(dashVectorClient, new DashVectorCollectionOptions() + { + Dimension = 1536 + }); + }) .Build(); }); diff --git a/samples/SK-DashScope.Sample/SK-DashScope.Sample.csproj b/samples/SK-DashScope.Sample/SK-DashScope.Sample.csproj index b1840c8..d91932e 100644 --- a/samples/SK-DashScope.Sample/SK-DashScope.Sample.csproj +++ b/samples/SK-DashScope.Sample/SK-DashScope.Sample.csproj @@ -17,6 +17,7 @@ + diff --git a/src/DashVector.SemanticKernel/DashVector.SemanticKernel.csproj b/src/DashVector.SemanticKernel/DashVector.SemanticKernel.csproj new file mode 100644 index 0000000..0ed682c --- /dev/null +++ b/src/DashVector.SemanticKernel/DashVector.SemanticKernel.csproj @@ -0,0 +1,16 @@ + + + + enable + enable + + + + + + + + + + + diff --git a/src/DashVector.SemanticKernel/DashVectorMemroyStore.cs b/src/DashVector.SemanticKernel/DashVectorMemroyStore.cs new file mode 100644 index 0000000..2bcc05b --- /dev/null +++ b/src/DashVector.SemanticKernel/DashVectorMemroyStore.cs @@ -0,0 +1,272 @@ +using DashVector.Enums; +using DashVector.Models; +using Microsoft.SemanticKernel.Memory; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json.Serialization; + +namespace DashVector.SemanticKernel +{ + /// + /// An IMemoryStore implementation that uses DashVector as the underlying storage. + /// + /// + public class DashVectorMemoryStore : IMemoryStore + { + static readonly Dictionary FieldsSchema = new() + { + [nameof(MemoryRecordMetadata.IsReference)] = FieldType.BOOL, + [nameof(MemoryRecordMetadata.ExternalSourceName)] = FieldType.STRING, + [nameof(MemoryRecordMetadata.Id)] = FieldType.STRING, + [nameof(MemoryRecordMetadata.Description)] = FieldType.STRING, + [nameof(MemoryRecordMetadata.Text)] = FieldType.STRING, + [nameof(MemoryRecordMetadata.AdditionalMetadata)] = FieldType.STRING, + }; + + static MemoryRecordMetadata ToMetaData(Dictionary fields) + { + return new MemoryRecordMetadata( + fields[nameof(MemoryRecordMetadata.IsReference)].GetValue(), + fields[nameof(MemoryRecordMetadata.Id)].GetValue(), + fields[nameof(MemoryRecordMetadata.Text)].GetValue(), + fields[nameof(MemoryRecordMetadata.Description)].GetValue(), + fields[nameof(MemoryRecordMetadata.ExternalSourceName)].GetValue(), + fields[nameof(MemoryRecordMetadata.AdditionalMetadata)].GetValue() + ); + } + static Dictionary ToFieldValues(MemoryRecordMetadata metadata) + { + return new Dictionary() + { + [nameof(MemoryRecordMetadata.IsReference)] = metadata.IsReference, + [nameof(MemoryRecordMetadata.ExternalSourceName)] = metadata.ExternalSourceName, + [nameof(MemoryRecordMetadata.Id)] = metadata.Id, + [nameof(MemoryRecordMetadata.Description)] = metadata.Description, + [nameof(MemoryRecordMetadata.Text)] = metadata.Text, + [nameof(MemoryRecordMetadata.AdditionalMetadata)] = metadata.AdditionalMetadata, + }; + } + + + + private readonly DashVectorClient client; + private readonly DashVectorCollectionOptions options; + + public DashVectorMemoryStore(DashVectorClient client, DashVectorCollectionOptions options) + { + this.client = client; + this.options = options; + } + + /// + public async Task CreateCollectionAsync(string collectionName, CancellationToken cancellationToken = default) + { + await client.CreateCollectionAsync(new Models.Requests.CreateCollectionRequest() + { + Name = collectionName, + DataType = options.DataType, + Dimension = options.Dimension, + Metric = options.Metric, + ExtraParams = options.ExtraParams, + FieldsSchema = FieldsSchema, + }, cancellationToken); + } + + /// + public async Task DeleteCollectionAsync(string collectionName, CancellationToken cancellationToken = default) + { + await client.DeleteCollectionAsync(collectionName, cancellationToken); + } + + /// + public async Task DoesCollectionExistAsync(string collectionName, CancellationToken cancellationToken = default) + { + try + { + var result = await client.DescribeCollectionAsync(collectionName, cancellationToken); + + return result.OutPut?.Status == CollectionStatus.SERVING; + } + catch { } + return false; + } + + /// + public async Task GetAsync(string collectionName, string key, bool withEmbedding = false, CancellationToken cancellationToken = default) + { + var results = await client.FetchDocAsync(new Models.Requests.FetchDocRequest() + { + Ids = [key] + }, collectionName, cancellationToken); + + if (results.Code == 0 && results.OutPut?.Count == 1) + { + var doc = results.OutPut.First().Value; + var metaData = ToMetaData(doc.Fields!); + + return MemoryRecord.FromMetadata(metaData, doc.Vector, doc.Id); + } + return null; + } + + /// + public async IAsyncEnumerable GetBatchAsync(string collectionName, IEnumerable keys, bool withEmbeddings = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var results = await client.FetchDocAsync(new Models.Requests.FetchDocRequest() + { + Ids = keys.ToList() + }, collectionName, cancellationToken); + + if (results.OutPut != null) + { + foreach (var (id, doc) in results.OutPut) + { + var metaData = ToMetaData(doc.Fields!); + + yield return MemoryRecord.FromMetadata(metaData, doc.Vector, doc.Id); + } + } + } + + /// + public async IAsyncEnumerable GetCollectionsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var collections = await client.GetCollectionListAsync(cancellationToken); + + if (collections.OutPut != null) + { + foreach (var collection in collections.OutPut) + { + yield return collection; + } + } + } + + /// + public async Task<(MemoryRecord, double)?> GetNearestMatchAsync(string collectionName, ReadOnlyMemory embedding, double minRelevanceScore = 0, bool withEmbedding = false, CancellationToken cancellationToken = default) + { + var result = await client.QueryDocAsync(new Models.Requests.QueryDocRequest() + { + IncludeVector = withEmbedding, + Vector = embedding.ToArray(), + TopK = 1, + }, collectionName, cancellationToken); + + if (result.OutPut?.Count != 1) + { + return null; + } + + var doc = result.OutPut[0]; + if (doc.Score < minRelevanceScore) + { + return null; + } + + var metadata = ToMetaData(doc.Fields!); + return (MemoryRecord.FromMetadata(metadata, doc.Vector, doc.Id), + doc.Score); + + } + + /// + public async IAsyncEnumerable<(MemoryRecord, double)> GetNearestMatchesAsync(string collectionName, ReadOnlyMemory embedding, int limit, double minRelevanceScore = 0, bool withEmbeddings = false, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var results = await client.QueryDocAsync(new Models.Requests.QueryDocRequest() + { + IncludeVector = withEmbeddings, + Vector = embedding.ToArray(), + TopK = limit, + }, collectionName, cancellationToken); + + if (results.OutPut?.Count > 0) + { + foreach (var doc in results.OutPut) + { + if (doc.Score < minRelevanceScore) + { + break; + } + + var metadata = ToMetaData(doc.Fields!); + yield return (MemoryRecord.FromMetadata(metadata, doc.Vector, doc.Id), + doc.Score); + } + } + } + + /// + public async Task RemoveAsync(string collectionName, string key, CancellationToken cancellationToken = default) + { + await client.DeleteDocAsync(new Models.Requests.DeleteDocRequest() + { + Ids = [key] + }, collectionName, cancellationToken); + } + + /// + public async Task RemoveBatchAsync(string collectionName, IEnumerable keys, CancellationToken cancellationToken = default) + { + await client.DeleteDocAsync(new Models.Requests.DeleteDocRequest() + { + Ids = keys.ToList() + }, collectionName, cancellationToken); + } + + /// + public async Task UpsertAsync(string collectionName, MemoryRecord record, CancellationToken cancellationToken = default) + { + var id = string.IsNullOrEmpty(record.Key) ? Guid.NewGuid().ToString("N") : record.Key; + await client.UpsertDocAsync(new Models.Requests.UpsertDocRequest() + { + Docs = [ + new Doc() + { + Id = id, + Fields = ToFieldValues(record.Metadata), + Vector = record.Embedding.ToArray() + } + ] + }, collectionName, cancellationToken); + return id; + } + + /// + public async IAsyncEnumerable UpsertBatchAsync(string collectionName, IEnumerable records, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var docs = new List(); + + foreach (var record in records) + { + var id = string.IsNullOrEmpty(record.Key) ? Guid.NewGuid().ToString("N") : record.Key; + + docs.Add(new Doc() + { + Id = id, + Fields = ToFieldValues(record.Metadata), + Vector = record.Embedding.ToArray() + }); + } + + await client.UpsertDocAsync(new Models.Requests.UpsertDocRequest() + { + Docs = docs, + }, collectionName, cancellationToken); + foreach (var doc in docs) + { + yield return doc.Id; + } + } + } + + public class DashVectorCollectionOptions + { + public int Dimension { get; set; } + public DataType DataType { get; set; } = DataType.FLOAT; + public string Metric { get; set; } = CollectionInfo.Metric.Cosine; + public Dictionary? ExtraParams { get; set; } + } +} diff --git a/src/DashVector.SemanticKernel/readme.md b/src/DashVector.SemanticKernel/readme.md new file mode 100644 index 0000000..96a3c54 --- /dev/null +++ b/src/DashVector.SemanticKernel/readme.md @@ -0,0 +1 @@ +# DashVector.SemanticKernel \ No newline at end of file diff --git a/src/DashVector/DashVector.csproj b/src/DashVector/DashVector.csproj index 132c02c..f80c10e 100644 --- a/src/DashVector/DashVector.csproj +++ b/src/DashVector/DashVector.csproj @@ -1,9 +1,14 @@ - + - net6.0 enable enable + e49d7ca5-270c-4339-8bc1-f8c4fe66a275 + + + + + diff --git a/src/DashVector/Enums/CollectionStatus.cs b/src/DashVector/Enums/CollectionStatus.cs index 485439c..7dc1274 100644 --- a/src/DashVector/Enums/CollectionStatus.cs +++ b/src/DashVector/Enums/CollectionStatus.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace DashVector.Enums { + [JsonConverter(typeof(JsonStringEnumConverter))] public enum CollectionStatus { INITIALIZED, diff --git a/src/DashVector/Models/CollectionMeta.cs b/src/DashVector/Models/CollectionMeta.cs index d68640e..250e9e1 100644 --- a/src/DashVector/Models/CollectionMeta.cs +++ b/src/DashVector/Models/CollectionMeta.cs @@ -45,7 +45,7 @@ public class CollectionMeta /// Fields, value: Float/Bool/INT/String /// [JsonPropertyName("fields_schema")] - public Dictionary FiledSchema { get; set; } + public Dictionary FiledSchema { get; set; } = []; /// /// PartitionName information diff --git a/src/DashVector/Models/CollectionStats.cs b/src/DashVector/Models/CollectionStats.cs index 654a0c4..e32d366 100644 --- a/src/DashVector/Models/CollectionStats.cs +++ b/src/DashVector/Models/CollectionStats.cs @@ -10,12 +10,13 @@ namespace DashVector.Models public class CollectionStats { [JsonPropertyName("total_doc_count")] - public string TotalDocCount { get; set; } + [JsonConverter(typeof(LongToStringConverter))] + public long TotalDocCount { get; set; } [JsonPropertyName("index_completeness")] public float IndexCompleteness { get; set; } [JsonPropertyName("partitions")] - public Dictionary Partitions { get; set; } + public Dictionary? Partitions { get; set; } } } diff --git a/src/DashVector/Models/Doc.cs b/src/DashVector/Models/Doc.cs index 129ef65..128db2d 100644 --- a/src/DashVector/Models/Doc.cs +++ b/src/DashVector/Models/Doc.cs @@ -21,7 +21,7 @@ public class Doc /// vector data /// [JsonPropertyName("vector")] - public List Vector { get; set; } + public float[] Vector { get; set; } = []; /// /// sparse vector data diff --git a/src/DashVector/Models/Requests/QueryDocRequest.cs b/src/DashVector/Models/Requests/QueryDocRequest.cs index 8f696b2..2d57f49 100644 --- a/src/DashVector/Models/Requests/QueryDocRequest.cs +++ b/src/DashVector/Models/Requests/QueryDocRequest.cs @@ -12,7 +12,7 @@ public class QueryDocRequest { [JsonPropertyName("vector")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? vector { get; set; } + public float[]? Vector { get; set; } [JsonPropertyName("sparse_vector")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/src/DashVector/Models/Responses/ErrorResponse.cs b/src/DashVector/Models/Responses/ErrorResponse.cs deleted file mode 100644 index aca03f1..0000000 --- a/src/DashVector/Models/Responses/ErrorResponse.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -namespace DashVector.Models.Responses -{ - public class ErrorResponse : ResponseBase - { - [JsonPropertyName("success")] - public bool Success { get; set; } - - [JsonPropertyName("httpStatusCode")] - public int HttpStatusCode { get; set; } - - [JsonPropertyName("requestId")] - public string RequestId { get; set; } - - [JsonPropertyName("accessDeniedDetail")] - public string AccessDeniedDetail { get; set; } - } -}