diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/EmbeddingCreateRequest.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/EmbeddingCreateRequest.cs new file mode 100644 index 00000000..b5dec5a8 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/EmbeddingCreateRequest.cs @@ -0,0 +1,79 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Embeddings; + +//TODO add model validation +//TODO check what is string or array for prompt,.. +public record EmbeddingCreateRequest +{ + /// + /// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs + /// in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in + /// length. + /// Unless your are embedding code, we suggest replacing newlines (`\n`) in your input with a single space, as we have + /// observed inferior results when newlines are present. + /// + /// + [JsonIgnore] + public List? InputAsList { get; set; } + + /// + /// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs + /// in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in + /// length. + /// Unless your are embedding code, we suggest replacing newlines (`\n`) in your input with a single space, as we have + /// observed inferior results when newlines are present. + /// + /// + [JsonIgnore] + public string? Input { get; set; } + + + [JsonPropertyName("input")] + public IList? InputCalculated + { + get + { + if (Input != null && InputAsList != null) + { + throw new ValidationException( + "Input and InputAsList can not be assigned at the same time. One of them is should be null."); + } + + if (Input != null) + { + return new List { Input }; + } + + return InputAsList; + } + } + + /// + /// ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your + /// available models, or see our [Model overview](/docs/models/overview) for descriptions of them. + /// + /// + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// + /// The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models. + /// + /// + [JsonPropertyName("dimensions")] + public int? Dimensions { get; set; } + + /// + /// The format to return the embeddings in. Can be either float or base64. + /// + /// + [JsonPropertyName("encoding_format")] + public string? EncodingFormat { get; set; } + + public IEnumerable Validate() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/EmbeddingCreateResponse.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/EmbeddingCreateResponse.cs new file mode 100644 index 00000000..6558cfbd --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/EmbeddingCreateResponse.cs @@ -0,0 +1,111 @@ +using System.Buffers; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Embeddings; + +public record EmbeddingCreateResponse : ThorBaseResponse +{ + [JsonPropertyName("model")] public string Model { get; set; } + + [JsonPropertyName("data")] public List Data { get; set; } = []; + + /// + /// 类型转换,如果类型是base64,则将float[]转换为base64,如果是空或是float和原始类型一样,则不转换 + /// + public void ConvertEmbeddingData(string? encodingFormat) + { + if (Data.Count == 0) + { + return; + } + + switch (encodingFormat) + { + // 判断第一个是否是float[],如果是则不转换 + case null or "float" when Data[0].Embedding is float[]: + return; + // 否则转换成float[] + case null or "float": + { + foreach (var embeddingResponse in Data) + { + if (embeddingResponse.Embedding is string base64) + { + embeddingResponse.Embedding = Convert.FromBase64String(base64); + } + } + + return; + } + // 判断第一个是否是string,如果是则不转换 + case "base64" when Data[0].Embedding is string: + return; + // 否则转换成base64 + case "base64": + { + foreach (var embeddingResponse in Data) + { + if (embeddingResponse.Embedding is JsonElement str) + { + if (str.ValueKind == JsonValueKind.Array) + { + var floats = str.EnumerateArray().Select(element => element.GetSingle()).ToArray(); + + embeddingResponse.Embedding = ConvertFloatArrayToBase64(floats); + } + } + else if (embeddingResponse.Embedding is IList doubles) + { + embeddingResponse.Embedding = ConvertFloatArrayToBase64(doubles.ToArray()); + } + } + + break; + } + } + } + + public static string ConvertFloatArrayToBase64(double[] floatArray) + { + // 将 float[] 转换成 byte[] + byte[] byteArray = ArrayPool.Shared.Rent(floatArray.Length * sizeof(float)); + try + { + Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length); + + // 将 byte[] 转换成 base64 字符串 + return Convert.ToBase64String(byteArray); + } + finally + { + ArrayPool.Shared.Return(byteArray); + } + } + + public static string ConvertFloatArrayToBase64(float[] floatArray) + { + // 将 float[] 转换成 byte[] + byte[] byteArray = ArrayPool.Shared.Rent(floatArray.Length * sizeof(float)); + try + { + Buffer.BlockCopy(floatArray, 0, byteArray, 0, floatArray.Length); + + // 将 byte[] 转换成 base64 字符串 + return Convert.ToBase64String(byteArray); + } + finally + { + ArrayPool.Shared.Return(byteArray); + } + } + + [JsonPropertyName("usage")] public ThorUsageResponse? Usage { get; set; } +} + +public record EmbeddingResponse +{ + [JsonPropertyName("index")] public int? Index { get; set; } + + [JsonPropertyName("embedding")] public object Embedding { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/ThorEmbeddingInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/ThorEmbeddingInput.cs new file mode 100644 index 00000000..f6f1a0c0 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/Embeddings/ThorEmbeddingInput.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Embeddings; + +public sealed class ThorEmbeddingInput +{ + [JsonPropertyName("model")] + public string Model { get; set; } + + [JsonPropertyName("input")] + public object Input { get; set; } + + [JsonPropertyName("encoding_format")] + public string EncodingFormat { get; set; } + + [JsonPropertyName("dimensions")] + public int? Dimensions { get; set; } + + [JsonPropertyName("user")] + public string? User { get; set; } +} + diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessage.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessage.cs index 7ec811e2..62c9edc4 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessage.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/OpenAi/ThorChatMessage.cs @@ -14,7 +14,6 @@ public class ThorChatMessage /// public ThorChatMessage() { - } /// @@ -74,20 +73,19 @@ public class ThorChatMessage { if (value is JsonElement str) { - if (str.ValueKind == JsonValueKind.String) - { - Content = value?.ToString(); - } - else if (str.ValueKind == JsonValueKind.Array) + if (str.ValueKind == JsonValueKind.Array) { Contents = JsonSerializer.Deserialize>(value?.ToString()); } } + else if (value is string strInput) + { + Content = strInput; + } else { Content = value?.ToString(); } - } } @@ -108,15 +106,14 @@ public class ThorChatMessage /// [JsonPropertyName("function_call")] public ThorChatMessageFunction? FunctionCall { get; set; } - + /// /// 【可选】推理内容 /// [JsonPropertyName("reasoning_content")] public string? ReasoningContent { get; set; } - - [JsonPropertyName("id")] - public string? Id { get; set; } + + [JsonPropertyName("id")] public string? Id { get; set; } /// /// 工具调用列表,模型生成的工具调用,例如函数调用。
@@ -164,14 +161,15 @@ public class ThorChatMessage /// 参与者的可选名称。提供模型信息以区分同一角色的参与者。 /// 工具调用参数列表 /// - public static ThorChatMessage CreateAssistantMessage(string content, string? name = null, List toolCalls = null) + public static ThorChatMessage CreateAssistantMessage(string content, string? name = null, + List toolCalls = null) { return new() { Role = ThorChatMessageRoleConst.Assistant, Content = content, Name = name, - ToolCalls=toolCalls, + ToolCalls = toolCalls, }; } @@ -187,7 +185,7 @@ public class ThorChatMessage { Role = ThorChatMessageRoleConst.Tool, Content = content, - ToolCallId= toolCallId + ToolCallId = toolCallId }; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs index 08c6a233..d67f7934 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/OpenApiService.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Volo.Abp.Application.Services; using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Embeddings; using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Images; using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Extensions; @@ -72,6 +73,20 @@ public class OpenApiService : ApplicationService await _aiBlacklistManager.VerifiyAiBlacklist(userId); await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input); } + + /// + /// 向量生成 + /// + /// + /// + [HttpPost("openApi/v1/embeddings")] + public async Task EmbeddingAsync([FromBody] ThorEmbeddingInput input, CancellationToken cancellationToken) + { + var httpContext = this._httpContextAccessor.HttpContext; + var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); + await _aiBlacklistManager.VerifiyAiBlacklist(userId); + await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input); + } /// diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelTypeEnum.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelTypeEnum.cs index e1167ce2..1e4fbd33 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelTypeEnum.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/ModelTypeEnum.cs @@ -1,8 +1,8 @@ namespace Yi.Framework.AiHub.Domain.Shared.Enums; - public enum ModelTypeEnum { Chat = 0, - Image = 1 + Image = 1, + Embedding = 2 } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/ITextEmbeddingService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/ITextEmbeddingService.cs new file mode 100644 index 00000000..8911fbf1 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/ITextEmbeddingService.cs @@ -0,0 +1,19 @@ +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Embeddings; +using Yi.Framework.AiHub.Domain.Shared.Dtos; + +namespace Yi.Framework.AiHub.Domain.AiGateWay; + +public interface ITextEmbeddingService +{ + /// + /// + /// + /// + /// + /// + /// + Task EmbeddingAsync( + EmbeddingCreateRequest createEmbeddingModel, + AiModelDescribe? aiModelDescribe = null, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorSiliconFlow/Embeddings/SiliconFlowTextEmbeddingService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorSiliconFlow/Embeddings/SiliconFlowTextEmbeddingService.cs new file mode 100644 index 00000000..42b25fd9 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiGateWay/Impl/ThorSiliconFlow/Embeddings/SiliconFlowTextEmbeddingService.cs @@ -0,0 +1,24 @@ +using System.Net.Http.Json; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Embeddings; +using Yi.Framework.AiHub.Domain.Shared.Dtos; + +namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings; + +public sealed class SiliconFlowTextEmbeddingService + : ITextEmbeddingService +{ + public async Task EmbeddingAsync( + EmbeddingCreateRequest createEmbeddingModel, + AiModelDescribe? options = null, + CancellationToken cancellationToken = default) + { + var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync( + options?.Endpoint.TrimEnd('/') + "/v1/embeddings", + createEmbeddingModel, options!.ApiKey); + + var result = + await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + + return result; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs index 042693a5..37c6a8c0 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; +using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -10,6 +11,7 @@ using Newtonsoft.Json.Serialization; using Volo.Abp.Domain.Services; using Yi.Framework.AiHub.Application.Contracts.Dtos; using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi; +using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Embeddings; using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Images; using Yi.Framework.AiHub.Domain.AiGateWay; using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions; @@ -276,19 +278,20 @@ public class AiGateWayManager : DomainService /// /// /// - public async Task CreateImageForStatisticsAsync(HttpContext context,Guid? userId,Guid? sessionId, ImageCreateRequest request) + public async Task CreateImageForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId, + ImageCreateRequest request) { try { var model = request.Model; if (string.IsNullOrEmpty(model)) model = "dall-e-2"; - + var modelDescribe = await GetModelAsync(model); - + // 获取渠道指定的实现类型的服务 var imageService = LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); - + var response = await imageService.CreateImage(request, modelDescribe); if (response.Error != null || response.Results.Count == 0) @@ -297,7 +300,7 @@ public class AiGateWayManager : DomainService } await context.Response.WriteAsJsonAsync(response); - + await _aiMessageManager.CreateUserMessageAsync(userId, sessionId, new MessageInputDto { @@ -322,4 +325,110 @@ public class AiGateWayManager : DomainService throw new UserFriendlyException(errorContent); } } + + + /// + /// 向量生成 + /// + /// + /// + /// + /// + /// + /// + public async Task EmbeddingForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId, + ThorEmbeddingInput input) + { + try + { + if (input == null) throw new Exception("模型校验异常"); + + using var embedding = + Activity.Current?.Source.StartActivity("向量模型调用"); + + var modelDescribe = await GetModelAsync(input.Model); + + // 获取渠道指定的实现类型的服务 + var embeddingService = + LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); + + var embeddingCreateRequest = new EmbeddingCreateRequest + { + Model = input.Model, + EncodingFormat = input.EncodingFormat + }; + + //dto进行转换,支持多种格式 + if (input.Input is JsonElement str) + { + if (str.ValueKind == JsonValueKind.Array) + { + var inputString = str.EnumerateArray().Select(x => x.ToString()).ToArray(); + embeddingCreateRequest.InputAsList = inputString.ToList(); + } + else + { + throw new Exception("Input,输入格式错误,非string或Array类型"); + } + } + else if (input.Input is string strInput) + { + embeddingCreateRequest.Input = strInput; + } + else + { + throw new Exception("Input,输入格式错误,未找到类型"); + } + + + var stream = + await embeddingService.EmbeddingAsync(embeddingCreateRequest, modelDescribe, context.RequestAborted); + + var usage = new ThorUsageResponse() + { + InputTokens = stream.Usage?.InputTokens ?? 0, + CompletionTokens = 0, + TotalTokens = stream.Usage?.InputTokens ?? 0 + }; + await context.Response.WriteAsJsonAsync(new + { + input.Model, + stream.Data, + stream.Error, + stream.ObjectTypeName, + Usage = usage + }); + + await _aiMessageManager.CreateUserMessageAsync(userId, sessionId, + new MessageInputDto + { + Content = string.Empty, + ModelId = input.Model, + TokenUsage = usage, + }); + + await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, + new MessageInputDto + { + Content = string.Empty, + ModelId = input.Model, + TokenUsage = usage + }); + + await _usageStatisticsManager.SetUsageAsync(userId, input.Model, usage); + } + catch (ThorRateLimitException) + { + context.Response.StatusCode = 429; + } + catch (UnauthorizedAccessException e) + { + context.Response.StatusCode = 401; + } + catch (Exception e) + { + var errorContent = $"嵌入Ai异常,异常信息:\n当前Ai模型:{input.Model}\n异常信息:{e.Message}\n异常堆栈:{e}"; + throw new UserFriendlyException(errorContent); + } + } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj index 17f70e7b..34f50862 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs index f143e6c9..eca30868 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs @@ -7,6 +7,7 @@ using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats; using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats; using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images; using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats; +using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings; using Yi.Framework.AiHub.Domain.Shared; using Yi.Framework.Mapster; @@ -35,6 +36,8 @@ namespace Yi.Framework.AiHub.Domain services.AddKeyedTransient( nameof(AzureOpenAIServiceImageService)); + + services.AddKeyedTransient(nameof(SiliconFlowTextEmbeddingService)); //ai模型特殊性兼容处理 Configure(options =>