diff --git a/BotSharp.sln b/BotSharp.sln
index ab198bed3..af8507972 100644
--- a/BotSharp.sln
+++ b/BotSharp.sln
@@ -35,6 +35,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "VectorStores", "VectorStore
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Qdrant", "src\Plugins\BotSharp.Plugin.Qdrant\BotSharp.Plugin.Qdrant.csproj", "{FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Chroma", "src\Plugins\BotSharp.Plugin.Chroma\BotSharp.Plugin.Chroma.csproj", "{0C097984-6C10-4F7E-A57D-95C5C9974697}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Visions", "Visions", "{B797EC3E-B5B9-4047-A6C8-6D330B7C6763}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.PaddleSharp", "src\Plugins\BotSharp.Plugin.PaddleSharp\BotSharp.Plugin.PaddleSharp.csproj", "{0308FBFD-57EB-4709-9AE4-A80D516AD84D}"
@@ -291,6 +293,18 @@ Global
{FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x64.Build.0 = Release|Any CPU
{FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x86.ActiveCfg = Release|Any CPU
{FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83}.Release|x86.Build.0 = Release|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Debug|x64.Build.0 = Debug|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Debug|x86.Build.0 = Debug|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Release|x64.ActiveCfg = Release|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Release|x64.Build.0 = Release|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Release|x86.ActiveCfg = Release|Any CPU
+ {0C097984-6C10-4F7E-A57D-95C5C9974697}.Release|x86.Build.0 = Release|Any CPU
{0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0308FBFD-57EB-4709-9AE4-A80D516AD84D}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -1028,6 +1042,7 @@ Global
{6D8D18A9-86D7-455E-81EC-9682C30AB7E7} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{53E7CD86-0D19-40D9-A0FA-AB4613837E89} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C}
{FE2E6CC1-EB80-4518-B3A3-CB373EDA6A83} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89}
+ {0C097984-6C10-4F7E-A57D-95C5C9974697} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89}
{B797EC3E-B5B9-4047-A6C8-6D330B7C6763} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C}
{0308FBFD-57EB-4709-9AE4-A80D516AD84D} = {B797EC3E-B5B9-4047-A6C8-6D330B7C6763}
{8300F66D-9EB8-438A-BF0F-70DFBE07D9DE} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0ec51a45d..0f0253710 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -62,6 +62,7 @@
+
diff --git a/src/Plugins/BotSharp.Plugin.Chroma/BotSharp.Plugin.Chroma.csproj b/src/Plugins/BotSharp.Plugin.Chroma/BotSharp.Plugin.Chroma.csproj
new file mode 100644
index 000000000..830720e23
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Chroma/BotSharp.Plugin.Chroma.csproj
@@ -0,0 +1,20 @@
+
+
+
+ $(TargetFramework)
+ enable
+ $(LangVersion)
+ $(BotSharpVersion)
+ $(GeneratePackageOnBuild)
+ $(GenerateDocumentationFile)
+ $(SolutionDir)packages
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Plugins/BotSharp.Plugin.Chroma/ChromaDb.cs b/src/Plugins/BotSharp.Plugin.Chroma/ChromaDb.cs
new file mode 100644
index 000000000..b124d7b1a
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Chroma/ChromaDb.cs
@@ -0,0 +1,609 @@
+using BotSharp.Abstraction.Utilities;
+using BotSharp.Abstraction.VectorStorage.Options;
+using ChromaDB.Client;
+using ChromaDB.Client.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+
+namespace BotSharp.Plugin.Chroma;
+
+public class ChromaDb : IVectorDb
+{
+ private ChromaClient? _client;
+ private readonly ChromaSetting _setting;
+ private readonly IServiceProvider _services;
+ private readonly ILogger _logger;
+
+ public ChromaDb(
+ ChromaSetting setting,
+ ILogger logger,
+ IServiceProvider services)
+ {
+ _setting = setting;
+ _logger = logger;
+ _services = services;
+ }
+
+ public string Provider => "Chroma";
+
+ private async Task GetClient()
+ {
+ if (_client == null)
+ {
+ var options = new ChromaConfigurationOptions
+ (
+ uri: _setting.Url,
+ tenant: _setting.Tenant.IfNullOrEmptyAs(null),
+ database: _setting.Database.IfNullOrEmptyAs(null),
+ chromaToken: _setting.ApiKey.IfNullOrEmptyAs(null)
+ );
+ var http = _services.GetRequiredService();
+ var client = new ChromaClient(options, http.CreateClient());
+
+ // On Chroma Cloud the API key gives the tenant and the database, as the CloudClient of the Python client
+ _client = options.IsChromaCloud ? await client.WithTenantAndDatabaseFromIdentityAsync() : client;
+ }
+ return _client;
+ }
+
+ #region Collection
+ public async Task DoesCollectionExist(string collectionName)
+ {
+ var client = await GetClient();
+ return await client.CollectionExistsAsync(collectionName);
+ }
+
+ public async Task CreateCollection(string collectionName, VectorCollectionCreateOptions options)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+
+ if (exist) return false;
+
+ try
+ {
+ // Create a new collection: Chroma takes the dimension from the first vector it stores
+ var client = await GetClient();
+ await client.CreateCollectionAsync(new ChromaCollectionDefinition(collectionName)
+ {
+ Configuration = new() { Space = ChromaSpace.Cosine }
+ });
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, $"Error when create collection (Name: {collectionName}, Dimension: {options.Dimension}).");
+ return false;
+ }
+ }
+
+ public async Task DeleteCollection(string collectionName)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+
+ if (!exist) return false;
+
+ var client = await GetClient();
+ await client.DeleteCollectionAsync(collectionName);
+ return true;
+ }
+
+ public async Task> GetCollections()
+ {
+ // List all the collections
+ var client = await GetClient();
+ var collections = await client.ListCollectionsAsync();
+ return collections.Select(x => x.Name).ToList();
+ }
+
+ public async Task GetCollectionDetails(string collectionName)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+
+ if (!exist)
+ {
+ return null;
+ }
+
+ var client = await GetClient();
+ var collection = client.GetCollectionClient(collectionName);
+ var count = (ulong)await collection.CountAsync();
+ var indexedCount = (ulong)await collection.CountAsync(ChromaReadLevel.IndexOnly);
+
+ return new VectorCollectionDetails
+ {
+ VectorsCount = count,
+ IndexedVectorsCount = indexedCount,
+ PointsCount = count
+ };
+ }
+ #endregion
+
+ #region Collection data
+ public async Task> GetPagedCollectionData(string collectionName, VectorFilter filter)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return new StringIdPagedItems();
+ }
+
+ // Chroma pages by offset: the next id is the offset of the next page
+ var where = BuildQueryFilter(filter.FilterGroups);
+ var offset = int.TryParse(filter.StartId, out var start) ? start : 0;
+ var include = ChromaGetInclude.Metadatas | ChromaGetInclude.Documents;
+ if (filter.WithVector)
+ {
+ include |= ChromaGetInclude.Embeddings;
+ }
+
+ var client = await GetClient();
+ var collection = client.GetCollectionClient(collectionName);
+ var totalCount = where == null
+ ? (ulong)await collection.CountAsync()
+ : (ulong)(await collection.GetAsync(where: where, include: ChromaGetInclude.None)).Count;
+ var entries = await collection.GetAsync(where: where, limit: filter.Size, offset: offset, include: include);
+
+ var points = entries.Select(x => new VectorCollectionData
+ {
+ Id = x.Id,
+ Payload = MapPayload(x.Metadata, x.Document, filter.Fields),
+ Vector = filter.WithVector ? x.Embedding?.ToArray() : null
+ }).ToList();
+
+ var nextOffset = offset + points.Count;
+ return new StringIdPagedItems
+ {
+ Count = totalCount,
+ NextId = points.Count == filter.Size && (ulong)nextOffset < totalCount ? nextOffset.ToString() : null,
+ Items = points
+ };
+ }
+
+ public async Task> GetCollectionData(string collectionName, IEnumerable ids, VectorQueryOptions? options = null)
+ {
+ if (ids.IsNullOrEmpty())
+ {
+ return Enumerable.Empty();
+ }
+
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return Enumerable.Empty();
+ }
+
+ var include = ChromaGetInclude.None;
+ if (options?.WithPayload == true)
+ {
+ include |= ChromaGetInclude.Metadatas | ChromaGetInclude.Documents;
+ }
+ if (options?.WithVector == true)
+ {
+ include |= ChromaGetInclude.Embeddings;
+ }
+
+ var client = await GetClient();
+ var collection = client.GetCollectionClient(collectionName);
+ var pointIds = ids.Select(x => x.ToString()).Distinct().ToList();
+ var entries = await collection.GetAsync(pointIds, include: include);
+ return entries.Select(x => new VectorCollectionData
+ {
+ Id = x.Id,
+ Payload = MapPayload(x.Metadata, x.Document),
+ Vector = x.Embedding?.ToArray()
+ });
+ }
+
+ public async Task Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary? payload = null)
+ {
+ // The text is the document of the record, the other payload values its metadata
+ var metadata = new Dictionary();
+
+ if (payload != null)
+ {
+ foreach (var item in payload)
+ {
+ if (item.Value == null || item.Key.IsEqualTo(KnowledgePayloadName.Text))
+ {
+ continue;
+ }
+
+ var value = item.Value.DataValue?.ConvertToString();
+ if (string.IsNullOrEmpty(value))
+ {
+ continue;
+ }
+
+ switch (item.Value.DataType)
+ {
+ case VectorPayloadDataType.Boolean when bool.TryParse(value, out var boolVal):
+ metadata[item.Key] = boolVal;
+ break;
+ case VectorPayloadDataType.Integer when long.TryParse(value, out var longVal):
+ metadata[item.Key] = longVal;
+ break;
+ case VectorPayloadDataType.Double when double.TryParse(value, out var doubleVal):
+ metadata[item.Key] = doubleVal;
+ break;
+ case VectorPayloadDataType.Datetime when DateTime.TryParse(value, out var dt):
+ metadata[item.Key] = dt.ToString("o");
+ break;
+ case VectorPayloadDataType.String:
+ metadata[item.Key] = value;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ var client = await GetClient();
+ var collection = client.GetCollectionClient(collectionName);
+
+ // The payload replaces the stored one, as in Qdrant: Chroma keeps the keys an upsert does not send, so a null deletes each of them
+ var stored = await collection.GetAsync(id.ToString(), include: ChromaGetInclude.Metadatas);
+ foreach (var key in stored?.Metadata?.Keys ?? Enumerable.Empty())
+ {
+ if (!metadata.ContainsKey(key))
+ {
+ metadata[key] = null!;
+ }
+ }
+
+ await collection.UpsertAsync(new ChromaRecords([id.ToString()])
+ {
+ Embeddings = [vector],
+ Metadatas = metadata.Count > 0 ? [metadata] : null,
+ Documents = [text]
+ });
+
+ return true;
+ }
+
+ public async Task> Search(string collectionName, float[] vector, VectorSearchOptions? options = null)
+ {
+ var results = new List();
+
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return results;
+ }
+
+ options ??= VectorSearchOptions.Default();
+ var where = BuildQueryFilter(options.FilterGroups);
+ var include = ChromaQueryInclude.Metadatas | ChromaQueryInclude.Documents | ChromaQueryInclude.Distances;
+ if (options.WithVector)
+ {
+ include |= ChromaQueryInclude.Embeddings;
+ }
+
+ var client = await GetClient();
+ var collection = client.GetCollectionClient(collectionName);
+ var entries = await collection.QueryAsync(new ChromaQuery([vector])
+ {
+ NResults = options.Limit.GetValueOrDefault(),
+ Where = where,
+ Include = include
+ });
+
+ // The collections have the cosine space: the score is the cosine similarity, 1 - the distance
+ results = entries.Single()
+ .Select(x => new VectorCollectionData
+ {
+ Id = x.Id,
+ Payload = MapPayload(x.Metadata, x.Document, options.Fields),
+ Score = 1 - x.Distance,
+ Vector = options.WithVector ? x.Embedding?.ToArray() : null
+ })
+ .Where(x => options.Confidence == null || x.Score >= options.Confidence)
+ .ToList();
+
+ return results;
+ }
+
+ public async Task DeleteCollectionData(string collectionName, List ids)
+ {
+ if (ids.IsNullOrEmpty()) return false;
+
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return false;
+ }
+
+ var client = await GetClient();
+ var collection = client.GetCollectionClient(collectionName);
+ await collection.DeleteAsync(ids.Select(x => x.ToString()).ToList());
+ return true;
+ }
+
+ public async Task DeleteCollectionAllData(string collectionName)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return false;
+ }
+
+ // Chroma deletes by ids or by filters: read the ids, then delete them
+ var client = await GetClient();
+ var collection = client.GetCollectionClient(collectionName);
+ var ids = (await collection.GetAsync(include: ChromaGetInclude.None)).Select(x => x.Id).ToList();
+ if (ids.Any())
+ {
+ await collection.DeleteAsync(ids);
+ }
+ return true;
+ }
+ #endregion
+
+ #region Payload index
+ public async Task CreateCollectionPayloadIndex(string collectionName, CollectionIndexOptions options)
+ {
+ // Chroma indexes every metadata key of a collection by default
+ return await DoesCollectionExist(collectionName);
+ }
+
+ public async Task DeleteCollectionPayloadIndex(string collectionName, CollectionIndexOptions options)
+ {
+ // Chroma does not turn off the index of a key once the collection exists
+ return await Task.FromResult(false);
+ }
+ #endregion
+
+ #region Snapshots
+ // Chroma has no snapshots of a collection
+ public async Task> GetCollectionSnapshots(string collectionName)
+ {
+ return await Task.FromResult(Enumerable.Empty());
+ }
+
+ public async Task CreateCollectionShapshot(string collectionName)
+ {
+ return await Task.FromResult(null);
+ }
+
+ public async Task DownloadCollectionSnapshot(string collectionName, string snapshotFileName)
+ {
+ return await Task.FromResult(BinaryData.Empty);
+ }
+
+ public async Task RecoverCollectionFromShapshot(string collectionName, string snapshotFileName, BinaryData snapshotData)
+ {
+ return await Task.FromResult(false);
+ }
+
+ public async Task DeleteCollectionShapshot(string collectionName, string snapshotName)
+ {
+ return await Task.FromResult(false);
+ }
+ #endregion
+
+
+ #region Private methods
+ #region Query filter
+ private ChromaWhereOperator? BuildQueryFilter(IEnumerable? filterGroups)
+ {
+ if (filterGroups.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ var groupConditions = filterGroups!.Select(x => BuildFilterGroupCondition(x))
+ .Where(c => c != null)
+ .ToList();
+
+ // Multiple groups are combined with AND, as in the other vector databases
+ return Combine(groupConditions, "and")?.Where;
+ }
+
+ private FilterCondition? BuildFilterGroupCondition(VectorFilterGroup group)
+ {
+ if (group.Filters.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ var subGroupConditions = group.Filters!
+ .Select(x => BuildSubGroupCondition(x))
+ .Where(c => c != null)
+ .ToList();
+
+ return Combine(subGroupConditions, group.LogicalOperator);
+ }
+
+ private FilterCondition? BuildSubGroupCondition(VectorFilterSubGroup subGroup)
+ {
+ if (subGroup.Operands.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ var operandConditions = subGroup.Operands
+ .Select(x => BuildOperandCondition(x))
+ .Where(c => c != null)
+ .ToList();
+
+ return Combine(operandConditions, subGroup.LogicalOperator);
+ }
+
+ private FilterCondition? Combine(List conditions, string logicalOperator)
+ {
+ if (conditions.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ var hasText = conditions.Any(x => x!.HasText);
+ var hasMetadata = conditions.Any(x => x!.HasMetadata);
+ if (logicalOperator.IsEqualTo("and"))
+ {
+ return new(conditions.Select(x => x!.Where).Aggregate((a, b) => a & b), hasText, hasMetadata);
+ }
+
+ // Chroma takes the filters on the document and the ones on the metadata together only with AND
+ if (hasText && hasMetadata)
+ {
+ throw new NotSupportedException($"Chroma does not combine a filter on {KnowledgePayloadName.Text} with a filter on another payload field by OR.");
+ }
+ return new(conditions.Select(x => x!.Where).Aggregate((a, b) => a | b), hasText, hasMetadata);
+ }
+
+ private FilterCondition? BuildOperandCondition(VectorFilterOperand operand)
+ {
+ FilterCondition? condition = null;
+
+ if (operand.Match != null)
+ {
+ condition = BuildMatchCondition(operand.Match);
+ }
+ else if (operand.Range != null)
+ {
+ condition = BuildRangeCondition(operand.Range);
+ }
+
+ return condition;
+ }
+
+ private FilterCondition? BuildMatchCondition(VectorFilterMatch match)
+ {
+ if (string.IsNullOrEmpty(match.Key) || match.Value == null)
+ {
+ return null;
+ }
+
+ // The text is the document of the record: an exact match is an anchored regular expression on it
+ if (match.Key.IsEqualTo(KnowledgePayloadName.Text))
+ {
+ if (match.Value.Length == 0)
+ {
+ throw new NotSupportedException($"Chroma does not filter {KnowledgePayloadName.Text} by an empty value.");
+ }
+
+ var pattern = $"^{EscapeRegex(match.Value)}$";
+ var document = match.Operator.IsEqualTo("eq")
+ ? ChromaWhereDocumentOperator.Regex(pattern)
+ : ChromaWhereDocumentOperator.NotRegex(pattern);
+ return new(ChromaWhereOperator.Document(document), HasText: true, HasMetadata: false);
+ }
+
+ object value = match.Value;
+ if (match.DataType == VectorPayloadDataType.Boolean
+ && bool.TryParse(match.Value, out var boolVal))
+ {
+ value = boolVal;
+ }
+ else if (match.DataType == VectorPayloadDataType.Integer
+ && long.TryParse(match.Value, out var longVal))
+ {
+ value = longVal;
+ }
+ else if (match.DataType == VectorPayloadDataType.Double
+ && double.TryParse(match.Value, out var doubleVal))
+ {
+ value = doubleVal;
+ }
+
+ var where = match.Operator.IsEqualTo("eq")
+ ? ChromaWhereOperator.Equal(match.Key, value)
+ : ChromaWhereOperator.NotEqual(match.Key, value);
+ return new(where, HasText: false, HasMetadata: true);
+ }
+
+ private FilterCondition? BuildRangeCondition(VectorFilterRange range)
+ {
+ if (string.IsNullOrEmpty(range.Key) || range.Conditions.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ // Chroma compares numbers only: a datetime is stored as text
+ if (range.DataType != VectorPayloadDataType.Integer
+ && range.DataType != VectorPayloadDataType.Double)
+ {
+ throw new NotSupportedException($"Chroma does not filter {range.DataType} values by range (Key: {range.Key}).");
+ }
+
+ var conditions = new List();
+ foreach (var condition in range.Conditions)
+ {
+ // An integer value stays an integer: Chroma Cloud does not compare a float with integer values
+ object value;
+ if (range.DataType == VectorPayloadDataType.Integer
+ && long.TryParse(condition.Value, out var longVal))
+ {
+ value = longVal;
+ }
+ else if (double.TryParse(condition.Value, out var doubleVal))
+ {
+ value = doubleVal;
+ }
+ else
+ {
+ continue;
+ }
+
+ switch (condition.Operator.ToLower())
+ {
+ case "lt":
+ conditions.Add(new(ChromaWhereOperator.LessThan(range.Key, value), HasText: false, HasMetadata: true));
+ break;
+ case "lte":
+ conditions.Add(new(ChromaWhereOperator.LessThanOrEqual(range.Key, value), HasText: false, HasMetadata: true));
+ break;
+ case "gt":
+ conditions.Add(new(ChromaWhereOperator.GreaterThan(range.Key, value), HasText: false, HasMetadata: true));
+ break;
+ case "gte":
+ conditions.Add(new(ChromaWhereOperator.GreaterThanOrEqual(range.Key, value), HasText: false, HasMetadata: true));
+ break;
+ }
+ }
+
+ return Combine(conditions, "and");
+ }
+ // The characters that the regular expressions of Chroma, of the Rust crate regex, take as syntax
+ private static string EscapeRegex(string text)
+ {
+ const string syntax = @"\.+*?()|[]{}^$#&-~";
+ return string.Concat(text.Select(c => syntax.Contains(c) ? $"\\{c}" : c.ToString()));
+ }
+
+ // A condition of the filter, and whether it has a condition on the text, which goes to the document, and on the metadata
+ private record FilterCondition(ChromaWhereOperator Where, bool HasText, bool HasMetadata);
+ #endregion
+
+ private Dictionary MapPayload(IReadOnlyDictionary? metadata, string? document, IEnumerable? fields = null)
+ {
+ var payload = new Dictionary();
+ if (document != null)
+ {
+ payload[KnowledgePayloadName.Text] = VectorPayloadValue.BuildStringValue(document);
+ }
+
+ foreach (var item in metadata ?? new Dictionary())
+ {
+ payload[item.Key] = item.Value switch
+ {
+ string s => VectorPayloadValue.BuildStringValue(s),
+ bool b => VectorPayloadValue.BuildBooleanValue(b),
+ long l => VectorPayloadValue.BuildIntegerValue(l),
+ int i => VectorPayloadValue.BuildIntegerValue(i),
+ double d => VectorPayloadValue.BuildDoubleValue(d),
+ float f => VectorPayloadValue.BuildDoubleValue(f),
+ _ => VectorPayloadValue.BuildUnkownValue(string.Empty)
+ };
+ }
+
+ if (fields.IsNullOrEmpty())
+ {
+ return payload;
+ }
+
+ // Only the payload fields asked for, as the other vector databases return
+ return payload.Where(x => fields!.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value);
+ }
+ #endregion
+}
diff --git a/src/Plugins/BotSharp.Plugin.Chroma/ChromaPlugin.cs b/src/Plugins/BotSharp.Plugin.Chroma/ChromaPlugin.cs
new file mode 100644
index 000000000..cab334cd5
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Chroma/ChromaPlugin.cs
@@ -0,0 +1,25 @@
+using BotSharp.Abstraction.Plugins;
+using BotSharp.Abstraction.Settings;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BotSharp.Plugin.Chroma;
+
+public class ChromaPlugin : IBotSharpPlugin
+{
+ public string Id => "c6e47c66-e02c-4e0a-b9c1-5ab64d74a9a1";
+ public string Name => "Chroma";
+ public string Description => "Open-source search and retrieval database for AI applications";
+ public string IconUrl => "https://raw.githubusercontent.com/chroma-core/chroma/main/docs/mintlify/images/light-logo.svg";
+ public void RegisterDI(IServiceCollection services, IConfiguration config)
+ {
+ services.AddScoped(provider =>
+ {
+ var settingService = provider.GetRequiredService();
+ return settingService.Bind("Chroma");
+ });
+
+ services.AddHttpClient();
+ services.AddScoped();
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Chroma/ChromaSetting.cs b/src/Plugins/BotSharp.Plugin.Chroma/ChromaSetting.cs
new file mode 100644
index 000000000..4d7c5eefe
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Chroma/ChromaSetting.cs
@@ -0,0 +1,24 @@
+namespace BotSharp.Plugin.Chroma;
+
+public class ChromaSetting
+{
+ ///
+ /// Server address, like http://localhost:8000, or https://api.trychroma.com for Chroma Cloud
+ ///
+ public string Url { get; set; } = string.Empty;
+
+ ///
+ /// API key of Chroma Cloud, or the token of a server with token authentication
+ ///
+ public string ApiKey { get; set; } = string.Empty;
+
+ ///
+ /// Tenant: on Chroma Cloud the API key gives it when empty, otherwise default_tenant
+ ///
+ public string Tenant { get; set; } = string.Empty;
+
+ ///
+ /// Database: on Chroma Cloud an API key for one database gives it when empty, otherwise default_database
+ ///
+ public string Database { get; set; } = string.Empty;
+}
diff --git a/src/Plugins/BotSharp.Plugin.Chroma/Using.cs b/src/Plugins/BotSharp.Plugin.Chroma/Using.cs
new file mode 100644
index 000000000..a68f82869
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Chroma/Using.cs
@@ -0,0 +1,10 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Linq;
+global using System.Threading.Tasks;
+global using BotSharp.Abstraction.VectorStorage;
+global using BotSharp.Abstraction.VectorStorage.Enums;
+global using BotSharp.Abstraction.VectorStorage.Models;
+global using BotSharp.Abstraction.Knowledges.Enums;
+global using BotSharp.Abstraction.Knowledges.Models;
+global using BotSharp.Abstraction.Knowledges.Options;
diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj
index 85aa1b1e2..8faed1223 100644
--- a/src/WebStarter/WebStarter.csproj
+++ b/src/WebStarter/WebStarter.csproj
@@ -73,6 +73,7 @@
+
diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json
index 823cc3110..3e4bf12c9 100644
--- a/src/WebStarter/appsettings.json
+++ b/src/WebStarter/appsettings.json
@@ -1437,6 +1437,13 @@
"ApiKey": ""
},
+ "Chroma": {
+ "Url": "",
+ "ApiKey": "",
+ "Tenant": "",
+ "Database": ""
+ },
+
"Graph": {
"BaseUrl": "",
"SearchPath": ""
@@ -1586,6 +1593,7 @@
"BotSharp.Plugin.Planner",
"BotSharp.Plugin.Graph",
"BotSharp.Plugin.Qdrant",
+ "BotSharp.Plugin.Chroma",
"BotSharp.Plugin.Membase",
"BotSharp.Plugin.ChatHub",
"BotSharp.Plugin.WeChat",